Passed
Pull Request — 1.x (#321)
by Akihito
03:22 queued 57s
created

OptionsMethods   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 230
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 93
dl 0
loc 230
rs 9.84
c 0
b 0
f 0
wmc 32

9 Methods

Rating   Name   Duplication   Size   Complexity  
A getMethodExtras() 0 17 4
A getInMap() 0 17 2
A filterInputAttributeParameters() 0 12 2
A __construct() 0 5 1
A getJsonSchema() 0 13 3
A getInsFromMethodAnnotations() 0 13 3
A getInsFromParameterAttributes() 0 18 4
B mergeParameterMetas() 0 43 9
A __invoke() 0 22 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\Resource\Options;
6
7
use BEAR\Resource\Annotation\Embed;
8
use BEAR\Resource\Annotation\JsonSchema;
9
use BEAR\Resource\Annotation\Link;
10
use BEAR\Resource\InputAttributeIterator;
11
use BEAR\Resource\ResourceObject;
12
use Ray\Aop\ReflectionMethod;
13
use Ray\Di\Di\Named;
14
use Ray\WebContextParam\Annotation\AbstractWebContextParam;
15
use Ray\WebContextParam\Annotation\CookieParam;
16
use Ray\WebContextParam\Annotation\EnvParam;
17
use Ray\WebContextParam\Annotation\FilesParam;
18
use Ray\WebContextParam\Annotation\FormParam;
19
use Ray\WebContextParam\Annotation\QueryParam;
20
use Ray\WebContextParam\Annotation\ServerParam;
21
use function array_filter;
22
use function array_merge;
23
use function array_unique;
24
use function assert;
25
use function class_exists;
26
use function file_exists;
27
use function file_get_contents;
28
use function in_array;
29
use function json_decode;
30
use const ARRAY_FILTER_USE_KEY;
31
use const JSON_THROW_ON_ERROR;
32
33
final class OptionsMethods
34
{
35
    /**
36
     * Constants for annotation name and "in" name
37
     */
38
    private const WEB_CONTEXT_NAME = [
39
        CookieParam::class => 'cookie',
40
        EnvParam::class => 'env',
41
        FormParam::class => 'formData',
42
        QueryParam::class => 'query',
43
        ServerParam::class => 'server',
44
        FilesParam::class => 'files',
45
    ];
46
47
    public function __construct(
48
        private readonly InputParamMetaInterface $inputParamMeta,
49
        #[Named('json_schema_dir')]
50
        private readonly string $schemaDir = '',
51
    ) {
52
    }
53
54
    /**
55
     * return array{summary?: string, description?: string, request: array, links: array, embed: array}
56
     *
57
     * @return array<int|string, array<mixed>|string>
58
     */
59
    public function __invoke(ResourceObject $ro, string $requestMethod): array
60
    {
61
        $method = new ReflectionMethod($ro::class, 'on' . $requestMethod);
62
        $ins = $this->getInMap($method);
63
        [$doc, $paramDoc] = (new OptionsMethodDocBolck())($method);
64
        $methodOption = $doc;
65
        $paramMetas = (new OptionsMethodRequest())($method, $paramDoc, $ins);
66
        $inputMetas = $this->inputParamMeta->get($method);
67
        $paramMetas = $this->mergeParameterMetas($paramMetas, $inputMetas, $method);
68
        $schema = $this->getJsonSchema($method);
69
        $request = $paramMetas ? ['request' => $paramMetas] : [];
70
        $methodOption += $request;
71
        if (! empty($schema)) {
72
            $methodOption += ['schema' => $schema];
73
        }
74
75
        $extras = $this->getMethodExtras($method);
76
        if (! empty($extras)) {
77
            $methodOption += $extras;
78
        }
79
80
        return $methodOption;
81
    }
82
83
    /**
84
     * @return (Embed|Link)[][]
85
     * @psalm-return array{links?: non-empty-list<Link>, embed?: non-empty-list<Embed>}
86
     * @phpstan-return (Embed|Link)[][]
87
     */
88
    private function getMethodExtras(ReflectionMethod $method): array
89
    {
90
        $extras = [];
91
        $annotations = $method->getAnnotations();
92
        foreach ($annotations as $annotation) {
93
            if ($annotation instanceof Link) {
94
                $extras['links'][] = $annotation;
95
            }
96
97
            if (! ($annotation instanceof Embed)) {
98
                continue;
99
            }
100
101
            $extras['embed'][] = $annotation;
102
        }
103
104
        return $extras;
105
    }
106
107
    /** @return array<string, string> */
108
    private function getInMap(ReflectionMethod $method): array
109
    {
110
        $ins = [];
111
        bc_for_annotation: {
112
            // @codeCoverageIgnoreStart
113
            $annotations = $method->getAnnotations();
114
            $ins = $this->getInsFromMethodAnnotations($annotations, $ins);
115
        if ($ins) {
116
            return $ins;
117
        }
118
            // @codeCoverageIgnoreEnd
119
        }
120
121
        /** @var array<string, string> $insParam */
122
        $insParam = $this->getInsFromParameterAttributes($method, $ins);
123
124
        return $insParam;
125
    }
126
127
    /** @return array<string, mixed> */
128
    private function getJsonSchema(ReflectionMethod $method): array
129
    {
130
        $schema = $method->getAnnotation(JsonSchema::class);
131
        if (! $schema instanceof JsonSchema) {
132
            return [];
133
        }
134
135
        $schemaFile = $this->schemaDir . '/' . $schema->schema;
136
        if (! file_exists($schemaFile)) {
137
            return [];
138
        }
139
140
        return (array) json_decode((string) file_get_contents($schemaFile), null, 512, JSON_THROW_ON_ERROR);
141
    }
142
143
    /**
144
     * @param array<object>         $annotations
145
     * @param array<string, string> $ins
146
     *
147
     * @return array<string, string>
148
     *
149
     * @codeCoverageIgnore BC for annotation
150
     */
151
    public function getInsFromMethodAnnotations(array $annotations, array $ins): array
152
    {
153
        foreach ($annotations as $annotation) {
154
            if (! ($annotation instanceof AbstractWebContextParam)) {
155
                continue;
156
            }
157
158
            $class = $annotation::class;
159
            assert(class_exists($class));
160
            $ins[$annotation->param] = self::WEB_CONTEXT_NAME[$class];
161
        }
162
163
        return $ins;
164
    }
165
166
    /**
167
     * @param array<string, string> $ins
168
     *
169
     * @return array<string, string>
170
     */
171
    public function getInsFromParameterAttributes(ReflectionMethod $method, array $ins): array|null
172
    {
173
        $parameters = $method->getParameters();
174
        foreach ($parameters as $parameter) {
175
            $attributes = $parameter->getAttributes();
176
            foreach ($attributes as $attribute) {
177
                $instance = $attribute->newInstance();
178
                if (! ($instance instanceof AbstractWebContextParam)) {
179
                    continue;
180
                }
181
182
                $class = $instance::class;
183
                assert(class_exists($class));
184
                $ins[$parameter->name] = self::WEB_CONTEXT_NAME[$class];
185
            }
186
        }
187
188
        return $ins;
189
    }
190
191
    /**
192
     * Merge parameter metadata from OptionsMethodRequest and InputParamMeta
193
     *
194
     * @param array{parameters?: array<string, array<string, mixed>>, required?: array<int, string>} $regularParams
195
     * @param array{parameters?: array<string, array<string, mixed>>, required?: array<int, string>} $inputParams
196
     *
197
     * @return array{parameters?: array<string, array<string, mixed>>, required?: array<int, string>}
198
     */
199
    private function mergeParameterMetas(array $regularParams, array $inputParams, \ReflectionMethod $method): array
200
    {
201
        if (empty($inputParams)) {
202
            return $regularParams;
203
        }
204
205
        // Filter out Input attribute parameters from regular parameters
206
        $filteredParams = $regularParams;
207
        if (isset($filteredParams['parameters'])) {
208
            $filteredParams['parameters'] = $this->filterInputAttributeParameters($filteredParams['parameters'], $method);
209
        }
210
211
        $merged = [];
212
213
        // Merge parameters
214
        $allParameters = [];
215
        if (isset($filteredParams['parameters'])) {
216
            $allParameters = array_merge($allParameters, $filteredParams['parameters']);
217
        }
218
219
        if (isset($inputParams['parameters'])) {
220
            $allParameters = array_merge($allParameters, $inputParams['parameters']);
221
        }
222
223
        if (! empty($allParameters)) {
224
            $merged['parameters'] = $allParameters;
225
        }
226
227
        // Merge required parameters
228
        $allRequired = [];
229
        if (isset($filteredParams['required'])) {
230
            $allRequired = array_merge($allRequired, $filteredParams['required']);
231
        }
232
233
        if (isset($inputParams['required'])) {
234
            $allRequired = array_merge($allRequired, $inputParams['required']);
235
        }
236
237
        if (! empty($allRequired)) {
238
            $merged['required'] = array_unique($allRequired);
239
        }
240
241
        return $merged;
242
    }
243
244
    /**
245
     * Filter out parameters that have Input attributes
246
     *
247
     * @param array<string, array<string, mixed>> $parameters
248
     *
249
     * @return array<string, array<string, mixed>>
250
     */
251
    private function filterInputAttributeParameters(array $parameters, \ReflectionMethod $method): array
252
    {
253
        $inputIterator = new InputAttributeIterator();
254
255
        $inputParamNames = [];
256
        foreach ($inputIterator($method) as $paramName => $param) {
257
            $inputParamNames[] = $paramName;
258
        }
259
260
        return array_filter($parameters, static function ($key) use ($inputParamNames) {
261
            return ! in_array($key, $inputParamNames, true);
262
        }, ARRAY_FILTER_USE_KEY);
263
    }
264
}
265