Completed
Push — master ( fbf566...079bfa )
by Michal
03:01 queued 01:16
created

OpenApiHandler::handle()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 87

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 87
ccs 0
cts 82
cp 0
rs 8.2836
c 0
b 0
f 0
cc 3
nc 4
nop 1
crap 12

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Tomaj\NetteApi\Handlers;
4
5
use InvalidArgumentException;
6
use Nette\Application\UI\InvalidLinkException;
7
use Nette\Http\IResponse;
8
use Nette\Http\Request;
9
use Symfony\Component\Yaml\Yaml;
10
use Tomaj\NetteApi\Api;
11
use Tomaj\NetteApi\ApiDecider;
12
use Tomaj\NetteApi\Authorization\BearerTokenAuthorization;
13
use Tomaj\NetteApi\Link\ApiLink;
14
use Tomaj\NetteApi\Output\JsonOutput;
15
use Tomaj\NetteApi\Output\RedirectOutput;
16
use Tomaj\NetteApi\Params\GetInputParam;
17
use Tomaj\NetteApi\Params\InputParam;
18
use Tomaj\NetteApi\Params\JsonInputParam;
19
use Tomaj\NetteApi\Params\RawInputParam;
20
use Tomaj\NetteApi\Response\JsonApiResponse;
21
use Tomaj\NetteApi\Response\ResponseInterface;
22
use Tomaj\NetteApi\Response\TextApiResponse;
23
24
class OpenApiHandler extends BaseHandler
25
{
26
    /** @var ApiDecider */
27
    private $apiDecider;
28
29
    /** @var ApiLink */
30
    private $apiLink;
31
32
    /** @var Request */
33
    private $request;
34
35
    private $initData = [];
36
37
    private $definitions = [];
38
39
    /**
40
     * OpenApiHandler constructor.
41
     * @param ApiDecider $apiDecider
42
     * @param ApiLink $apiLink
43
     * @param Request $request
44
     * @param array $initData - structured data for initialization response
45
     */
46
    public function __construct(
47
        ApiDecider $apiDecider,
48
        ApiLink $apiLink,
49
        Request $request,
50
        array $initData = []
51
    ) {
52
        parent::__construct();
53
        $this->apiDecider = $apiDecider;
54
        $this->apiLink = $apiLink;
55
        $this->request = $request;
56
        $this->initData = $initData;
57
    }
58
59
    public function params(): array
60
    {
61
        return [
62
            (new GetInputParam('format'))->setAvailableValues(['json', 'yaml'])->setDescription('Response format'),
63
        ];
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function description(): string
70
    {
71
        return 'Open API';
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function tags(): array
78
    {
79
        return ['openapi'];
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85
    public function handle(array $params): ResponseInterface
86
    {
87
        $version = $this->getEndpoint()->getVersion();
88
        $apis = $this->getApis($version);
89
        $scheme = $this->request->getUrl()->getScheme();
90
        $host = $this->request->getUrl()->getHost();
91
        $baseUrl = $scheme . '://' . $host;
92
        $basePath = $this->getBasePath($apis, $baseUrl);
93
94
        $data = [
95
            'openapi' => '3.0.0',
96
            'info' => [
97
                'version' => (string)$version,
98
                'title' => 'Nette API',
99
            ],
100
            'servers' => [
101
                [
102
                    'url' => $scheme . '://' . $host . $basePath,
103
                ],
104
            ],
105
            'components' => [
106
                'securitySchemes' => [
107
                    'Bearer' => [
108
                        'type' => 'http',
109
                        'scheme' => 'bearer',
110
                    ],
111
                ],
112
                'schemas' => [
113
                    'ErrorWrongInput' => [
114
                        'type' => 'object',
115
                        'properties' => [
116
                            'status' => [
117
                                'type' => 'string',
118
                                'enum' => ['error'],
119
                            ],
120
                            'message' => [
121
                                'type' => 'string',
122
                                'enum' => ['Wrong input'],
123
                            ],
124
                        ],
125
                        'required' => ['status', 'message'],
126
                    ],
127
                    'ErrorForbidden' => [
128
                        'type' => 'object',
129
                        'properties' => [
130
                            'status' => [
131
                                'type' => 'string',
132
                                'enum' => ['error'],
133
                            ],
134
                            'message' => [
135
                                'type' => 'string',
136
                                'enum' => ['Authorization header HTTP_Authorization is not set', 'Authorization header contains invalid structure'],
137
                            ],
138
                        ],
139
                        'required' => ['status', 'message'],
140
                    ],
141
                    'InternalServerError' => [
142
                        'type' => 'object',
143
                        'properties' => [
144
                            'status' => [
145
                                'type' => 'string',
146
                                'enum' => ['error'],
147
                            ],
148
                            'message' => [
149
                                'type' => 'string',
150
                                'enum' => ['Internal server error'],
151
                            ],
152
                        ],
153
                        'required' => ['status', 'message'],
154
                    ],
155
                ],
156
            ],
157
158
            'paths' => $this->getPaths($apis, $baseUrl, $basePath),
159
        ];
160
161
        if (!empty($this->definitions)) {
162
            $data['components']['schemas'] = array_merge($this->definitions, $data['components']['schemas']);
163
        }
164
165
        $data = array_replace_recursive($data, $this->initData);
166
167
        if ($params['format'] === 'yaml') {
168
            return new TextApiResponse(IResponse::S200_OK, Yaml::dump($data, PHP_INT_MAX, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE));
169
        }
170
        return new JsonApiResponse(IResponse::S200_OK, $data);
171
    }
172
173
    private function getApis(int $version): array
174
    {
175
        return array_filter($this->apiDecider->getApis(), function (Api $api) use ($version) {
176
            return $version === $api->getEndpoint()->getVersion();
177
        });
178
    }
179
180
    /**
181
     * @param Api[] $versionApis
182
     * @param string $baseUrl
183
     * @param string $basePath
184
     * @return array
185
     * @throws InvalidLinkException
186
     */
187
    private function getPaths(array $versionApis, string $baseUrl, string $basePath): array
188
    {
189
        $list = [];
190
        foreach ($versionApis as $api) {
191
            $handler = $api->getHandler();
192
            $path = str_replace([$baseUrl, $basePath], '', $this->apiLink->link($api->getEndpoint()));
193
            $responses = [];
194
            foreach ($handler->outputs() as $output) {
195
                if ($output instanceof JsonOutput) {
196
                    $schema = $this->transformSchema(json_decode($output->getSchema(), true));
197
                    $responses[$output->getCode()] = [
198
                        'description' => $output->getDescription(),
199
                        'content' => [
200
                            'application/json' => [
201
                                'schema' => $schema,
202
                            ],
203
                        ]
204
                    ];
205
                }
206
207
                if ($output instanceof RedirectOutput) {
208
                    $responses[$output->getCode()] = [
209
                        'description' => 'Redirect',
210
                        'headers' => [
211
                            'Location' => [
212
                                'description' => $output->getDescription(),
213
                                'schema' => [
214
                                    'type' => 'string',
215
                                ]
216
                            ],
217
                        ]
218
                    ];
219
                }
220
            }
221
222
            $responses[IResponse::S400_BAD_REQUEST] = [
223
                'description' => 'Bad request',
224
                'content' => [
225
                    'application/json' => [
226
                        'schema' => [
227
                            '$ref' => '#/components/schemas/ErrorWrongInput',
228
                        ],
229
                    ]
230
                ],
231
            ];
232
233
            $responses[IResponse::S403_FORBIDDEN] = [
234
                'description' => 'Operation forbidden',
235
                'content' => [
236
                    'application/json' => [
237
                        'schema' => [
238
                            '$ref' => '#/components/schemas/ErrorForbidden',
239
                        ],
240
                    ],
241
                ],
242
            ];
243
244
            $responses[IResponse::S500_INTERNAL_SERVER_ERROR] = [
245
                'description' => 'Internal server error',
246
                'content' => [
247
                    'application/json' => [
248
                        'schema' => [
249
                            '$ref' => '#/components/schemas/InternalServerError',
250
                        ],
251
                    ],
252
                ],
253
            ];
254
255
            $settings = [
256
                'summary' => $handler->summary(),
257
                'description' => $handler->description(),
258
                'tags' => $handler->tags(),
259
            ];
260
261
            if ($handler->deprecated()) {
262
                $settings['deprecated'] = true;
263
            }
264
265
            $parameters = $this->createParamsList($handler);
266
            if (!empty($parameters)) {
267
                $settings['parameters'] = $parameters;
268
            }
269
270
            $requestBody = $this->createRequestBody($handler);
271
            if (!empty($requestBody)) {
272
                $settings['requestBody'] = $requestBody;
273
            }
274
275
            if ($api->getAuthorization() instanceof BearerTokenAuthorization) {
276
                $settings['security'] = [
277
                    [
278
                        'Bearer' => [],
279
                    ],
280
                ];
281
            }
282
            $settings['responses'] = $responses;
283
            $list[$path][strtolower($api->getEndpoint()->getMethod())] = $settings;
284
        }
285
        return $list;
286
    }
287
288
    private function getBasePath(array $apis, string $baseUrl): string
289
    {
290
        $basePath = '';
291
        foreach ($apis as $handler) {
292
            $basePath = $this->getLongestCommonSubstring($basePath, $this->apiLink->link($handler->getEndpoint()));
293
        }
294
        return rtrim(str_replace($baseUrl, '', $basePath), '/');
295
    }
296
297
    private function getLongestCommonSubstring($path1, $path2)
298
    {
299
        if ($path1 === null) {
300
            return $path2;
301
        }
302
        $commonSubstring = '';
303
        $shortest = min(strlen($path1), strlen($path2));
304
        for ($i = 0; $i <= $shortest; ++$i) {
305
            if (substr($path1, 0, $i) !== substr($path2, 0, $i)) {
306
                break;
307
            }
308
            $commonSubstring = substr($path1, 0, $i);
309
        }
310
        return $commonSubstring;
311
    }
312
313
    /**
314
     * Create array with params for specified handler
315
     *
316
     * @param ApiHandlerInterface $handler
317
     *
318
     * @return array
319
     */
320
    private function createParamsList(ApiHandlerInterface $handler)
321
    {
322
        $parameters = [];
323
        foreach ($handler->params() as $param) {
324
            if ($param->getType() !== InputParam::TYPE_GET) {
325
                continue;
326
            }
327
328
            $schema = [
329
                'type' => $param->isMulti() ? 'array' : 'string',
330
            ];
331
332
            $parameter = [
333
                'name' => $param->getKey() . ($param->isMulti() ? '[]' : ''),
334
                'in' => $this->createIn($param->getType()),
335
                'required' => $param->isRequired(),
336
                'description' => $param->getDescription(),
337
            ];
338
339
            if ($param->isMulti()) {
340
                $schema['items'] = ['type' => 'string'];
341
            }
342
            if ($param->getAvailableValues()) {
343
                $schema['enum'] = $param->getAvailableValues();
344
            }
345
            if ($param->getExample() || $param->getDefault()) {
346
                $schema['example'] = $param->getExample() ?: $param->getDefault();
347
            }
348
349
            $parameter['schema'] = $schema;
350
351
            $parameters[] = $parameter;
352
        }
353
        return $parameters;
354
    }
355
356
    private function createRequestBody(ApiHandlerInterface $handler)
357
    {
358
        $postParams = [
359
            'properties' => [],
360
            'required' => [],
361
        ];
362
        $postParamsExample = [];
363
        foreach ($handler->params() as $param) {
364
            if ($param instanceof JsonInputParam) {
365
                $schema = json_decode($param->getSchema(), true);
366
                if ($param->getExample()) {
367
                    $schema['example'] = $param->getExample();
368
                }
369
                return [
370
                    'description' => $param->getDescription(),
371
                    'required' => $param->isRequired(),
372
                    'content' => [
373
                        'application/json' => [
374
                            'schema' => $this->transformSchema($schema),
375
                        ],
376
                    ],
377
                ];
378
            }
379
            if ($param instanceof RawInputParam) {
380
                return [
381
                    'description' => $param->getDescription(),
382
                    'required' => $param->isRequired(),
383
                    'content' => [
384
                        'text/plain' => [
385
                            'schema' => [
386
                                'type' => 'string',
387
                            ],
388
                        ],
389
                    ],
390
                ];
391
            }
392
            if ($param->getType() === InputParam::TYPE_POST) {
393
                $property = [
394
                    'type' => $param->isMulti() ? 'array' : 'string',
395
                    'description' => $param->getDescription(),
396
                ];
397
                if ($param->isMulti()) {
398
                    $property['items'] = ['type' => 'string'];
399
                }
400
                if ($param->getAvailableValues()) {
401
                    $property['enum'] = $param->getAvailableValues();
402
                }
403
404
                $postParams['properties'][$param->getKey() . ($param->isMulti() ? '[]' : '')] = $property;
405
                if ($param->isRequired()) {
406
                    $postParams['required'][] = $param->getKey() . ($param->isMulti() ? '[]' : '');
407
                }
408
409
                if ($param->getExample() || $param->getDefault()) {
410
                    $postParamsExample[$param->getKey()] = $param->getExample() ?: $param->getDefault();
411
                }
412
            }
413
        }
414
415
        if (!empty($postParams['properties'])) {
416
            $postParamsSchema = [
417
                'type' => 'object',
418
                'properties' => $postParams['properties'],
419
                'required' => $postParams['required'],
420
            ];
421
422
            if ($postParamsExample) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $postParamsExample of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
423
                $postParamsSchema['example'] = $postParamsExample;
424
            }
425
426
            return [
427
                'required' => true,
428
                'content' => [
429
                    'application/x-www-form-urlencoded' => [
430
                        'schema' => $postParamsSchema,
431
                    ],
432
                ],
433
            ];
434
        }
435
436
        return null;
437
    }
438
439
    private function createIn($type)
440
    {
441
        if ($type == InputParam::TYPE_GET) {
442
            return 'query';
443
        }
444
        if ($type == InputParam::TYPE_COOKIE) {
445
            return 'cookie';
446
        }
447
        return 'body';
448
    }
449
450
    private function transformSchema(array $schema)
451
    {
452
        $this->transformTypes($schema);
453
454
        if (isset($schema['definitions'])) {
455
            foreach ($schema['definitions'] as $name => $definition) {
456
                $this->addDefinition($name, $this->transformSchema($definition));
457
            }
458
            unset($schema['definitions']);
459
        }
460
        return json_decode(str_replace('#/definitions/', '#/components/schemas/', json_encode($schema, JSON_UNESCAPED_SLASHES)), true);
461
    }
462
463
    private function transformTypes(array &$schema)
464
    {
465
        foreach ($schema as $key => &$value) {
466
            if ($key === 'type' && is_array($value)) {
467
                if (count($value) === 2 && in_array('null', $value)) {
468
                    unset($value[array_search('null', $value)]);
469
                    $value = implode(',', $value);
470
                    $schema['nullable'] = true;
471
                } else {
472
                    throw new InvalidArgumentException('Type cannot be array and if so, one element have to be "null"');
473
                }
474
            } elseif (is_array($value)) {
475
                $this->transformTypes($value);
476
            }
477
        }
478
    }
479
480
    private function addDefinition($name, $definition)
481
    {
482
        if (isset($this->definitions[$name])) {
483
            throw new InvalidArgumentException('Definition with name ' . $name . ' already exists. Rename it or use existing one.');
484
        }
485
        $this->definitions[$name] = $definition;
486
    }
487
}
488