Passed
Pull Request — 1.x (#321)
by Akihito
02:34
created

OptionsMethods::mergeParameterMetas()   B

Complexity

Conditions 9
Paths 129

Size

Total Lines 43
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

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