Completed
Push — master ( 83fca8...0d9c6e )
by Andrii
10:43
created

src/Service/OpenApi/OpenAPIGenerator.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
declare(strict_types=1);
3
4
namespace hiapi\Service\OpenApi;
5
6
use cebe\openapi\spec\Components;
7
use cebe\openapi\spec\OpenApi;
8
use cebe\openapi\spec\Operation;
9
use cebe\openapi\spec\PathItem;
10
use cebe\openapi\spec\Response;
11
use cebe\openapi\spec\Responses;
12
use cebe\openapi\spec\Schema;
13
use cebe\openapi\spec\SecurityRequirement;
14
use cebe\openapi\spec\SecurityScheme;
15
use cebe\openapi\spec\Server;
16
use cebe\openapi\spec\ServerVariable;
17
use Generator;
18
use hiapi\commands\Reflection\BaseCommandReflection;
19
use hiapi\Core\Endpoint\EndpointRepository;
20
use hiapi\endpoints\Module\InOutControl\VO\Collection;
21
use hiapi\exceptions\ConfigurationException;
22
use hiapi\validators\Reflection\ValidatorReflection;
23
use yii\validators\EachValidator;
24
use yii\validators\InlineValidator;
25
use yii\validators\RequiredValidator;
26
use yii\validators\SafeValidator;
27
use yii\validators\Validator;
28
use ReflectionObject;
29
30
/**
31
 * Class OpenAPIGenerator generates OpenAPI documentation.
32
 *
33
 * This class represents a general implementation.
34
 *
35
 * @author Dmytro Naumenko <[email protected]>
36
 */
37
class OpenAPIGenerator
38
{
39
    /** @psalm-var array<string, SecurityScheme> */
40
    private array $securitySchemes;
0 ignored issues
show
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_ARRAY, expecting T_FUNCTION or T_CONST
Loading history...
41
    private EndpointRepository $endpointRepository;
42
    /** @psalm-var list<string> */
43
    private array $hosts;
44
    private array $apiInfo;
45
46
    public function __construct(EndpointRepository $endpointRepository, array $options = [])
47
    {
48
        $this->endpointRepository = $endpointRepository;
49
50
        $this->hosts = $options['hosts'] ?? [];
51
        $this->securitySchemes = $options['securitySchemes'] ?? [];
52
        $this->apiInfo = $options['apiInfo'] ?? [];
53
    }
54
55
    public function __invoke(): OpenApi
56
    {
57
        return $this->createOpenAPI();
58
    }
59
60
    private function createOpenAPI(): OpenApi
61
    {
62
        return new OpenApi([
63
            'openapi' => '3.0.2',
64
            'info' => $this->apiInfo,
65
            'paths' => iterator_to_array($this->generatePaths(), true),
66
            'servers' => iterator_to_array($this->buildServers(), false),
67
            'components' => $this->buildComponents(),
68
        ]);
69
    }
70
71
    /**
72
     * @psalm-return Generator<int, Server>
73
     */
74
    private function buildServers(): Generator
75
    {
76
        $isDev = YII_ENV_DEV;
77
78
        foreach($this->hosts as $host) {
79
            yield new Server([
80
                'url' => "{protocol}://$host/",
81
                'variables' => [
82
                    'protocol' => new ServerVariable([
83
                        'enum' => ['http', 'https'],
84
                        'default' => $isDev ? 'http' : 'https',
85
                    ]),
86
                ],
87
            ]);
88
        }
89
    }
90
91
    /**
92
     * @psalm-return Generator<int, PathItem>
93
     */
94
    private function generatePaths(): Generator
95
    {
96
        foreach ($this->endpointRepository->getEndpointNames() as $name) {
97
            try {
98
                $endpoint = $this->endpointRepository->getByName($name);
99
                $input = $endpoint->inputType;
100
101
                yield "/$name" => new PathItem(array_filter([
102
                    'post' => new Operation([
103
                        'tags' => [], // TODO: parse endpoint name
104
                        'summary' => $endpoint->description,
105
                        'security' => iterator_to_array($this->generateSecuritySchemas($endpoint), true),
106
                        'requestBody' => [
107
                            'required' => true,
108
                            'content' => [
109
                                'application/x-www-form-urlencoded' => [
110
                                    'schema' => $this->generateRequestSchema($endpoint),
111
                                ],
112
                            ],
113
                        ],
114
                        'responses' => new Responses([
115
                            'default' => new Response([
116
                                'description' => '',
117
                                // TODO: response example
118
                            ]),
119
                        ]),
120
                    ]),
121
                ]));
122
            } catch (ConfigurationException $exception) {
123
            }
124
        }
125
    }
126
127
    private function buildComponents(): Components
128
    {
129
        return new Components([
130
            'schemas' => $this->validationSchemas(),
131
            'securitySchemes' => $this->securitySchemes,
132
        ]);
133
    }
134
135
    /**
136
     * @return Schema[]
137
     */
138
    private function validationSchemas(): array
139
    {
140
        $result = [];
141
        foreach ($this->metValidators as $name => $className) {
142
            $reflection = ValidatorReflection::fromClassname($className);
143
144
            $result[$name] = new Schema(array_filter([
145
                'pattern' => $reflection->getPattern(),
146
                'type' => 'string',
147
                'description' => $reflection->getSummary(),
148
                'example' => $reflection->getExample(),
149
            ]));
150
        }
151
152
        return $result;
153
    }
154
155
    /**
156
     * @param \hiapi\Core\Endpoint\Endpoint $endpoint
157
     * @psalm-return Generator<int, SecurityScheme>
158
     */
159
    private function generateSecuritySchemas(\hiapi\Core\Endpoint\Endpoint $endpoint): Generator
160
    {
161
        if (!empty($endpoint->permissions)) {
162
            foreach ($this->securitySchemes as $name => $_) {
163
                yield new SecurityRequirement([
164
                    $name => $endpoint->permissions,
165
                ]);
166
            }
167
        }
168
    }
169
170
    private function generateRequestSchema(\hiapi\Core\Endpoint\Endpoint $endpoint): Schema
171
    {
172
        $properties = $required = [];
173
174
        $inputType = $endpoint->inputType;
175
        if ($inputType instanceof Collection) {
176
            // TODO: describe as a nested object
177
            $inputType = $inputType->getEntriesClass();
178
        }
179
        /** @psalm-var class-string<BaseCommand> $inputType */
180
        $reflection = BaseCommandReflection::fromClassname($inputType);
181
182
        foreach ($reflection->getAttributes() as $attribute) {
183
            $rules = $reflection->getAttributeValidationRules($attribute);
184
185
            $schemas = [];
186
            foreach ($rules as $key => $validator) {
187
                if ($validator instanceof RequiredValidator) {
188
                    $required[] = $attribute;
189
                }
190
                if (($rule = $this->ruleNameByValidator($validator)) === null) {
191
                    continue;
192
                }
193
194
                $schemas[$key] = new Schema([
195
                    '$ref' => '#/components/schemas/' . $rule,
196
                ]);
197
            }
198
            if (count($schemas) === 0) {
199
                continue; // Not validated attributes are not a part of a public API
200
            }
201
202
            if (count($schemas) === 1) {
203
                $properties[$attribute] = reset($schemas);
204
            } else {
205
                $properties[$attribute] = new Schema(['allOf' => $schemas]);
206
            }
207
        }
208
209
        return new Schema([
210
            'properties' => $properties,
211
            'required' => array_unique($required),
212
        ]);
213
    }
214
215
    /** @psalm-var array<string, class-string<Validator>> */
216
    private array $metValidators = [];
217
218
    private function ruleNameByValidator(Validator $validator): ?string
219
    {
220
        $skippedValidators = [
221
            RequiredValidator::class => 'Is represented by OpenAPI required syntax',
222
            EachValidator::class => 'Is represented by OpenAPI array syntax',
223
            InlineValidator::class => 'Could not be inspected and should not be used',
224
            SafeValidator::class => 'Means nothing in OpenAPI',
225
        ];
226
        if (isset($skippedValidators[get_class($validator)])) {
227
            return null;
228
        }
229
230
        $name = (new ReflectionObject($validator))->getShortName();
231
        if (!isset($this->metValidators[$name])) {
232
            $this->metValidators[$name] = get_class($validator);
233
        }
234
235
        return $name;
236
    }
237
}
238