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

OptionsMethods::mergeParameterMetas()   B

Complexity

Conditions 9
Paths 129

Size

Total Lines 43
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 21
nc 129
nop 3
dl 0
loc 43
rs 7.8138
c 0
b 0
f 0
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
22
use function array_filter;
23
use function array_merge;
24
use function array_unique;
25
use function file_exists;
26
use function file_get_contents;
27
use function in_array;
28
use function json_decode;
29
30
use const ARRAY_FILTER_USE_KEY;
31
use const JSON_THROW_ON_ERROR;
32
33
/**
34
 * @psalm-type WebContextKey = class-string<AbstractWebContextParam>
35
 * @psalm-type WebContextValue = 'cookie'|'env'|'formData'|'query'|'server'|'files'
36
 * @psalm-type OptionParamDoc = array{description?: string, embed?: mixed, links?: mixed, request?: mixed, schema?: mixed, summary?: string}
37
 */
38
final class OptionsMethods
39
{
40
    /**
41
     * Constants for annotation name and "in" name
42
     */
43
    private const WEB_CONTEXT_NAME = [
44
        CookieParam::class => 'cookie',
45
        EnvParam::class => 'env',
46
        FormParam::class => 'formData',
47
        QueryParam::class => 'query',
48
        ServerParam::class => 'server',
49
        FilesParam::class => 'files',
50
    ];
51
52
    public function __construct(
53
        private readonly InputParamMetaInterface $inputParamMeta,
54
        #[Named('json_schema_dir')]
55
        private readonly string $schemaDir = '',
56
    ) {
57
    }
58
59
    /** @return OptionParamDoc */
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\Options\OptionParamDoc was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
60
    public function __invoke(ResourceObject $ro, string $requestMethod): array
61
    {
62
        $method = new ReflectionMethod($ro::class, 'on' . $requestMethod);
63
        $ins = $this->getInMap($method);
64
        [$doc, $paramDoc] = (new OptionsMethodDocBolck())($method);
65
        $methodOption = $doc;
66
        $paramMetas = (new OptionsMethodRequest())($method, $paramDoc, $ins);
67
        $inputMetas = $this->inputParamMeta->get($method);
68
        $paramMetas = $this->mergeParameterMetas($paramMetas, $inputMetas, $method);
69
        $schema = $this->getJsonSchema($method);
70
        $request = $paramMetas ? ['request' => $paramMetas] : [];
71
        $methodOption += $request;
72
        if (! empty($schema)) {
73
            $methodOption += ['schema' => $schema];
74
        }
75
76
        $extras = $this->getMethodExtras($method);
77
        if (! empty($extras)) {
78
            $methodOption += $extras;
79
        }
80
81
        /** @var OptionParamDoc $methodOption */
82
        return $methodOption;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $methodOption returns the type BEAR\Resource\Options\OptionParamDoc which is incompatible with the type-hinted return array.
Loading history...
83
    }
84
85
    /**
86
     * @return (Embed|Link)[][]
87
     * @psalm-return array{links?: non-empty-list<Link>, embed?: non-empty-list<Embed>}
88
     * @phpstan-return (Embed|Link)[][]
89
     */
90
    private function getMethodExtras(ReflectionMethod $method): array
91
    {
92
        $extras = [];
93
        $annotations = $method->getAnnotations();
94
        foreach ($annotations as $annotation) {
95
            if ($annotation instanceof Link) {
96
                $extras['links'][] = $annotation;
97
            }
98
99
            if (! ($annotation instanceof Embed)) {
100
                continue;
101
            }
102
103
            $extras['embed'][] = $annotation;
104
        }
105
106
        return $extras;
107
    }
108
109
    /** @return array<string, string> */
110
    private function getInMap(ReflectionMethod $method): array
111
    {
112
        $ins = [];
113
        bc_for_annotation: {
114
            // @codeCoverageIgnoreStart
115
            $annotations = $method->getAnnotations();
116
            $ins = $this->getInsFromMethodAnnotations($annotations, $ins);
117
        if ($ins) {
118
            return $ins;
119
        }
120
            // @codeCoverageIgnoreEnd
121
        }
122
123
        /** @var array<string, string> $insParam */
124
        $insParam = $this->getInsFromParameterAttributes($method, $ins);
125
126
        return $insParam;
127
    }
128
129
    /** @return array<string, mixed> */
130
    private function getJsonSchema(ReflectionMethod $method): array
131
    {
132
        $schema = $method->getAnnotation(JsonSchema::class);
133
        if (! $schema instanceof JsonSchema) {
134
            return [];
135
        }
136
137
        $schemaFile = $this->schemaDir . '/' . $schema->schema;
138
        if (! file_exists($schemaFile)) {
139
            return [];
140
        }
141
142
        /** @var array<string, mixed> $schema */
143
        $schema = (array) json_decode((string) file_get_contents($schemaFile), null, 512, JSON_THROW_ON_ERROR);
144
145
        return $schema;
146
    }
147
148
    /**
149
     * @param array<object>         $annotations
150
     * @param array<string, string> $ins
151
     *
152
     * @return array<string, string>
153
     *
154
     * @codeCoverageIgnore BC for annotation
155
     */
156
    public function getInsFromMethodAnnotations(array $annotations, array $ins): array
157
    {
158
        foreach ($annotations as $annotation) {
159
            if (! ($annotation instanceof AbstractWebContextParam)) {
160
                continue;
161
            }
162
163
            $class = $annotation::class;
164
            if (! isset(self::WEB_CONTEXT_NAME[$class])) {
165
                continue;
166
            }
167
168
            $ins[$annotation->param] = self::WEB_CONTEXT_NAME[$class];
169
        }
170
171
        return $ins;
172
    }
173
174
    /**
175
     * @param array<string, string> $ins
176
     *
177
     * @return array<string, string>
178
     */
179
    public function getInsFromParameterAttributes(ReflectionMethod $method, array $ins): array|null
180
    {
181
        $parameters = $method->getParameters();
182
        foreach ($parameters as $parameter) {
183
            $attributes = $parameter->getAttributes();
184
            foreach ($attributes as $attribute) {
185
                $instance = $attribute->newInstance();
186
                if (! ($instance instanceof AbstractWebContextParam)) {
187
                    continue;
188
                }
189
190
                $class = $instance::class;
191
                if (! isset(self::WEB_CONTEXT_NAME[$class])) {
192
                    continue;
193
                }
194
195
                $webContextName = self::WEB_CONTEXT_NAME[$class];
196
                $ins[$parameter->name] = $webContextName;
197
            }
198
        }
199
200
        return $ins;
201
    }
202
203
    /**
204
     * Merge parameter metadata from OptionsMethodRequest and InputParamMeta
205
     *
206
     * @param array{parameters?: array<string, array<string, mixed>>, required?: array<int, string>} $regularParams
207
     * @param array{parameters?: array<string, array<string, mixed>>, required?: array<int, string>} $inputParams
208
     *
209
     * @return array{parameters?: array<string, array<string, mixed>>, required?: array<int, string>}
210
     */
211
    private function mergeParameterMetas(array $regularParams, array $inputParams, \ReflectionMethod $method): array
212
    {
213
        if (empty($inputParams)) {
214
            return $regularParams;
215
        }
216
217
        // Filter out Input attribute parameters from regular parameters
218
        $filteredParams = $regularParams;
219
        if (isset($filteredParams['parameters'])) {
220
            $filteredParams['parameters'] = $this->filterInputAttributeParameters($filteredParams['parameters'], $method);
221
        }
222
223
        $merged = [];
224
225
        // Merge parameters
226
        $allParameters = [];
227
        if (isset($filteredParams['parameters'])) {
228
            $allParameters = array_merge($allParameters, $filteredParams['parameters']);
229
        }
230
231
        if (isset($inputParams['parameters'])) {
232
            $allParameters = array_merge($allParameters, $inputParams['parameters']);
233
        }
234
235
        if (! empty($allParameters)) {
236
            $merged['parameters'] = $allParameters;
237
        }
238
239
        // Merge required parameters
240
        $allRequired = [];
241
        if (isset($filteredParams['required'])) {
242
            $allRequired = array_merge($allRequired, $filteredParams['required']);
243
        }
244
245
        if (isset($inputParams['required'])) {
246
            $allRequired = array_merge($allRequired, $inputParams['required']);
247
        }
248
249
        if (! empty($allRequired)) {
250
            $merged['required'] = array_unique($allRequired);
251
        }
252
253
        return $merged;
254
    }
255
256
    /**
257
     * Filter out parameters that have Input attributes
258
     *
259
     * @param array<string, array<string, mixed>> $parameters
260
     *
261
     * @return array<string, array<string, mixed>>
262
     */
263
    private function filterInputAttributeParameters(array $parameters, \ReflectionMethod $method): array
264
    {
265
        $inputIterator = new InputAttributeIterator();
266
267
        $inputParamNames = [];
268
        foreach ($inputIterator($method) as $paramName => $param) {
269
            unset($param);
270
            $inputParamNames[] = $paramName;
271
        }
272
273
        return array_filter($parameters, static function ($key) use ($inputParamNames) {
274
            return ! in_array($key, $inputParamNames, true);
275
        }, ARRAY_FILTER_USE_KEY);
276
    }
277
}
278