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

OptionsMethods   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 189
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 79
c 2
b 0
f 0
dl 0
loc 189
rs 10
wmc 25

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getMethodExtras() 0 17 4
A getInMap() 0 17 2
A __construct() 0 5 1
A getJsonSchema() 0 16 3
A getInsFromMethodAnnotations() 0 16 4
A getInsFromParameterAttributes() 0 20 4
A mergeParameterMetas() 0 18 3
A __invoke() 0 23 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\ResourceObject;
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_merge;
22
use function array_unique;
23
use function array_values;
24
use function assert;
25
use function file_exists;
26
use function file_get_contents;
27
use function json_decode;
28
29
use const JSON_THROW_ON_ERROR;
30
31
/**
32
 * @psalm-type WebContextKey = class-string<AbstractWebContextParam>
33
 * @psalm-type WebContextValue = 'cookie'|'env'|'formData'|'query'|'server'|'files'
34
 * @psalm-type OptionParamDoc = array{description?: string, embed?: mixed, links?: mixed, request?: mixed, schema?: mixed, summary?: string}
35
 * @psalm-import-type OptionMethodMeta from InputParamMetaInterface
36
 */
37
final class OptionsMethods
38
{
39
    /**
40
     * Constants for annotation name and "in" name
41
     */
42
    private const WEB_CONTEXT_NAME = [
43
        CookieParam::class => 'cookie',
44
        EnvParam::class => 'env',
45
        FormParam::class => 'formData',
46
        QueryParam::class => 'query',
47
        ServerParam::class => 'server',
48
        FilesParam::class => 'files',
49
    ];
50
51
    public function __construct(
52
        private readonly InputParamMetaInterface $inputParamMeta,
53
        #[Named('json_schema_dir')]
54
        private readonly string $schemaDir = '',
55
    ) {
56
    }
57
58
    /** @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...
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);
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
        /** @var OptionParamDoc $methodOption */
81
        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...
82
    }
83
84
    /**
85
     * @return (Embed|Link)[][]
86
     * @psalm-return array{links?: non-empty-list<Link>, embed?: non-empty-list<Embed>}
87
     * @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
        /** @var array<string, mixed> $schema */
142
        $schema = (array) json_decode((string) file_get_contents($schemaFile), null, 512, JSON_THROW_ON_ERROR);
143
144
        return $schema;
145
    }
146
147
    /**
148
     * @param array<object>         $annotations
149
     * @param array<string, string> $ins
150
     *
151
     * @return array<string, string>
152
     *
153
     * @codeCoverageIgnore BC for annotation
154
     */
155
    public function getInsFromMethodAnnotations(array $annotations, array $ins): array
156
    {
157
        foreach ($annotations as $annotation) {
158
            if (! ($annotation instanceof AbstractWebContextParam)) {
159
                continue;
160
            }
161
162
            $class = $annotation::class;
163
            if (! isset(self::WEB_CONTEXT_NAME[$class])) {
164
                continue;
165
            }
166
167
            $ins[$annotation->param] = self::WEB_CONTEXT_NAME[$class];
168
        }
169
170
        return $ins;
171
    }
172
173
    /**
174
     * @param array<string, string> $ins
175
     *
176
     * @return array<string, string>
177
     */
178
    public function getInsFromParameterAttributes(ReflectionMethod $method, array $ins): array|null
179
    {
180
        $parameters = $method->getParameters();
181
        foreach ($parameters as $parameter) {
182
            $attributes = $parameter->getAttributes();
183
            foreach ($attributes as $attribute) {
184
                $instance = $attribute->newInstance();
185
                if (! ($instance instanceof AbstractWebContextParam)) {
186
                    continue;
187
                }
188
189
                $class = $instance::class;
190
                assert(isset(self::WEB_CONTEXT_NAME[$class]), 'WebContextParam annotation must be one of the known types');
191
192
                $webContextName = self::WEB_CONTEXT_NAME[$class];
193
                $ins[$parameter->name] = $webContextName;
194
            }
195
        }
196
197
        return $ins;
198
    }
199
200
    /**
201
     * Merge parameter metadata from OptionsMethodRequest and InputParamMeta
202
     *
203
     * @param OptionMethodMeta $regularParams
204
     * @param OptionMethodMeta $inputParams
0 ignored issues
show
Bug introduced by
The type BEAR\Resource\Options\OptionMethodMeta 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...
205
     *
206
     * @return OptionMethodMeta
207
     */
208
    private function mergeParameterMetas(array $regularParams, array $inputParams): array
209
    {
210
        $regularParameters = $regularParams['parameters'] ?? [];
211
        $inputParameters   = $inputParams['parameters'] ?? [];
212
213
        $regularRequired = $regularParams['required'] ?? [];
214
        $inputRequired   = $inputParams['required'] ?? [];
215
216
        $parameters = array_merge($regularParameters, $inputParameters);
217
        $required = array_values(array_unique(array_merge($regularRequired, $inputRequired)));
218
219
        if ($parameters === [] && $required === []) {
220
            return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type BEAR\Resource\Options\OptionMethodMeta.
Loading history...
221
        }
222
223
        return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('parameters...required' => $required) returns the type array<string,array> which is incompatible with the documented return type BEAR\Resource\Options\OptionMethodMeta.
Loading history...
224
            'parameters' => $parameters,
225
            'required'   => $required,
226
        ];
227
    }
228
}
229