Passed
Push — master ( 94f1d9...2d4769 )
by Pieter
02:23
created

OpenApiSpecGenerator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 8
dl 0
loc 18
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace W2w\Lib\Apie\OpenApiSchema;
4
5
use W2w\Lib\Apie\ApiResourceMetadataFactory;
6
use W2w\Lib\Apie\IdentifierExtractor;
7
use W2w\Lib\Apie\Resources\ApiResourcesInterface;
8
use W2w\Lib\Apie\ClassResourceConverter;
9
use erasys\OpenApi\Spec\v3 as OASv3;
10
use W2w\Lib\Apie\Retrievers\SearchFilterProviderInterface;
11
12
/**
13
 * Class that generated an OpenAPI spec from a list of API resources.
14
 */
15
class OpenApiSpecGenerator
16
{
17
    private $apiResources;
18
19
    private $converter;
20
21
    private $info;
22
23
    private $schemaGenerator;
24
25
    private $apiResourceMetadataFactory;
26
27
    private $identifierExtractor;
28
29
    private $baseUrl;
30
31
    private $addSpecsHook;
32
33
    public function __construct(
34
        ApiResourcesInterface $apiResources,
35
        ClassResourceConverter $converter,
36
        OASv3\Info $info,
37
        SchemaGenerator $schemaGenerator,
38
        ApiResourceMetadataFactory $apiResourceMetadataFactory,
39
        IdentifierExtractor $identifierExtractor,
40
        string $baseUrl,
41
        ?callable $addSpecsHook = null
42
    ) {
43
        $this->apiResources = $apiResources;
44
        $this->converter = $converter;
45
        $this->info = $info;
46
        $this->schemaGenerator = $schemaGenerator;
47
        $this->apiResourceMetadataFactory = $apiResourceMetadataFactory;
48
        $this->identifierExtractor = $identifierExtractor;
49
        $this->baseUrl = $baseUrl;
50
        $this->addSpecsHook = $addSpecsHook;
51
    }
52
53
    private function getIdentifierKey(string $className): ?string
54
    {
55
        $context = $this->apiResourceMetadataFactory->getMetadata($className)->getContext();
56
        return $this->identifierExtractor->getIdentifierKeyOfClass($className, $context);
57
    }
58
59
    /**
60
     * Gets an OpenAPI spec document.
61
     *
62
     * @return OASv3\Document
63
     */
64
    public function getOpenApiSpec(): OASv3\Document
65
    {
66
        $paths = [];
67
        foreach ($this->apiResources->getApiResources() as $apiResourceClass) {
68
            $path = $this->converter->normalize($apiResourceClass);
69
            $identifierName = $this->getIdentifierKey($apiResourceClass);
70
            $paths['/' . $path] = $this->convertAllToPathItem($apiResourceClass, $path);
71
            if ($identifierName) {
72
                $paths['/' . $path . '/{' . $identifierName . '}'] = $this->convertToPathItem($apiResourceClass, $path, $identifierName);
73
            }
74
        }
75
76
        $stringSchema = new OASv3\Schema(['type' => 'string']);
77
        $stringOrIntSchema = new OASv3\Schema(['oneOf' => [$stringSchema, new OASv3\Schema(['type' => 'integer'])]]);
78
        $stringArraySchema = new OASv3\Schema(['type' => 'array', 'items' => $stringSchema]);
79
80
        $errorSchema = new OASv3\Reference('#/components/schemas/Error');
81
82
        $validationErrorSchema = new OASv3\Schema([
83
            'type'       => 'object',
84
            'properties' => [
85
                'type'    => $stringSchema,
86
                'message' => $stringSchema,
87
                'code'    => $stringOrIntSchema,
88
                'trace'   => $stringSchema,
89
                'errors'  => new OASv3\Schema([
90
                    'type'       => 'object',
91
                    'additionalProperties' => $stringArraySchema
92
                ]),
93
            ],
94
            'xml' => new OASv3\Xml(['name' => 'response']),
95
        ]);
96
97
        $doc = new OASv3\Document(
98
            $this->info,
99
            $paths,
100
            '3.0.1',
101
            [
102
                'servers' => [
103
                    new OASv3\Server($this->baseUrl),
104
                ],
105
                'components' => new OASv3\Components([
106
                    'schemas' => [
107
                        'Error' => new OASv3\Schema([
108
                            'type'       => 'object',
109
                            'properties' => [
110
                                'type'    => $stringSchema,
111
                                'message' => $stringSchema,
112
                                'code'    => $stringOrIntSchema,
113
                                'trace'   => $stringSchema,
114
                            ],
115
                            'xml' => new OASv3\Xml(['name' => 'response']),
116
                        ]),
117
                    ],
118
                    'headers' => [
119
                        'x-ratelimit-limit' => new Oasv3\Header(
120
                            'Request limit per hour',
121
                            [
122
                                'example' => 100,
123
                                'schema'  => new OASv3\Schema([
124
                                    'type' => 'integer',
125
                                ]),
126
                            ]
127
                        ),
128
                        'x-ratelimit-remaining' => new Oasv3\Header(
129
                            'Request limit per hour',
130
                            [
131
                                'example' => 94,
132
                                'schema'  => new OASv3\Schema([
133
                                    'type' => 'integer',
134
                                ]),
135
                            ]
136
                        ),
137
                    ],
138
                    'responses' => [
139
                        'InvalidFormat' => new OASv3\Response(
140
                            'The body input could not be parsed',
141
                            [
142
                                'application/json' => new OASv3\MediaType(
143
                                    [
144
                                        'schema' => $errorSchema,
145
                                    ]
146
                                ),
147
                                'application/xml' => new OASv3\MediaType(
148
                                    [
149
                                        'schema' => $errorSchema,
150
                                    ]
151
                                ),
152
                            ]
153
                        ),
154
                        'ValidationError' => new OASv3\Response(
155
                            'The body input was in a proper format, but the input values were not valid',
156
                            [
157
                                'application/json' => new OASv3\MediaType(
158
                                    [
159
                                        'schema' => $validationErrorSchema,
160
                                    ]
161
                                ),
162
                                'application/xml' => new OASv3\MediaType(
163
                                    [
164
                                        'schema' => $validationErrorSchema,
165
                                    ]
166
                                ),
167
                            ]
168
                        ),
169
                        'TooManyRequests' => new OASv3\Response(
170
                            'Too many requests per seconds were sent',
171
                            [
172
                                'application/json' => new OASv3\MediaType(
173
                                    [
174
                                        'schema' => $errorSchema,
175
                                    ]
176
                                ),
177
                                'application/xml' => new OASv3\MediaType(
178
                                    [
179
                                        'schema' => $errorSchema,
180
                                    ]
181
                                ),
182
                            ]
183
                        ),
184
                        'MaintenanceMode' => new OASv3\Response(
185
                            'App is in maintenance mode',
186
                            [
187
                                'application/json' => new OASv3\MediaType(
188
                                    [
189
                                        'schema' => $errorSchema,
190
                                    ]
191
                                ),
192
                                'application/xml' => new OASv3\MediaType(
193
                                    [
194
                                        'schema' => $errorSchema,
195
                                    ]
196
                                ),
197
                            ]
198
                        ),
199
                        'NotFound' => new OASv3\Response(
200
                            'Response when resource could not be found',
201
                            [
202
                                'application/json' => new OASv3\MediaType(
203
                                    [
204
                                        'schema' => $errorSchema,
205
                                    ]
206
                                ),
207
                                'application/xml' => new OASv3\MediaType(
208
                                    [
209
                                        'schema' => $errorSchema,
210
                                    ]
211
                                ),
212
                            ]
213
                        ),
214
                        'NotAuthorized' => new OASv3\Response(
215
                            'You have no permission to do this call',
216
                            [
217
                                'application/json' => new OASv3\MediaType(
218
                                    [
219
                                        'schema' => $errorSchema,
220
                                    ]
221
                                ),
222
                                'application/xml' => new OASv3\MediaType(
223
                                    [
224
                                        'schema' => $errorSchema,
225
                                    ]
226
                                ),
227
                            ]
228
                        ),
229
                        'InternalError' => new OASv3\Response(
230
                            'An internal error occured',
231
                            [
232
                                'application/json' => new OASv3\MediaType(
233
                                    [
234
                                        'schema' => $errorSchema,
235
                                    ]
236
                                ),
237
                                'application/xml' => new OASv3\MediaType(
238
                                    [
239
                                        'schema' => $errorSchema,
240
                                    ]
241
                                ),
242
                            ]
243
                        ),
244
                        'ServerDependencyError' => new OASv3\Response(
245
                            'The server required an external response which threw an error',
246
                            [
247
                                'application/json' => new OASv3\MediaType(
248
                                    [
249
                                        'schema' => $errorSchema,
250
                                    ]
251
                                ),
252
                                'application/xml' => new OASv3\MediaType(
253
                                    [
254
                                        'schema' => $errorSchema,
255
                                    ]
256
                                ),
257
                            ]
258
                        ),
259
                    ],
260
                ]),
261
            ]
262
        );
263
        if (is_callable($this->addSpecsHook)) {
264
            $res = call_user_func($this->addSpecsHook, $doc);
265
            if ($res instanceof OASv3\Document) {
266
                return $res;
267
            }
268
        }
269
270
        return $doc;
271
    }
272
273
    /**
274
     * Returns the default HTTP headers we generated for every REST api call.
275
     *
276
     * @return array
277
     */
278
    private function getDefaultHeaders(): array
279
    {
280
        return [
281
            'x-ratelimit-limit'     => new OASv3\Reference('#/components/headers/x-ratelimit-limit'),
282
            'x-ratelimit-remaining' => new OASv3\Reference('#/components/headers/x-ratelimit-remaining'),
283
        ];
284
    }
285
286
    /**
287
     * Returns the content OpenAPI spec for a resource class and a certain operation.
288
     *
289
     * @param string $apiResourceClass
290
     * @param string $operation
291
     * @return OASv3\MediaType[]
292
     */
293
    private function convertToContent(string $apiResourceClass, string $operation): array
294
    {
295
        $readWrite = $this->determineReadWrite($operation);
296
        $jsonSchema = $this->schemaGenerator->createSchema($apiResourceClass, $operation, [$operation, $readWrite]);
297
        $xmlSchema = $this->schemaGenerator->createSchema($apiResourceClass, $operation, [$operation, $readWrite]);
298
        $xmlSchema->xml = new OASv3\Xml(['name' => 'item']);
299
300
        return [
301
            'application/json' => new OASv3\MediaType(
302
                [
303
                    'schema' => $jsonSchema,
304
                ]
305
            ),
306
            'application/xml' => new OASv3\MediaType(
307
                [
308
                    'schema' => $xmlSchema,
309
                ]
310
            ),
311
        ];
312
    }
313
314
    /**
315
     * Returns the content OpenAPI spec for a resource class when it returns an array of resources.
316
     *
317
     * @param string $apiResourceClass
318
     * @param string $operation
319
     * @return OASv3\MediaType[]
320
     */
321
    private function convertToContentArray(string $apiResourceClass, string $operation): array
322
    {
323
        $readWrite = $this->determineReadWrite($operation);
324
        $jsonSchema = $this->schemaGenerator->createSchema($apiResourceClass, $operation, [$operation, $readWrite]);
325
        $xmlSchema = $this->schemaGenerator->createSchema($apiResourceClass, $operation, [$operation, $readWrite]);
326
        $xmlSchema->xml = new OASv3\Xml(['name' => 'item']);
327
328
        return [
329
            'application/json' => new OASv3\MediaType(
330
                [
331
                    'schema' => new OASv3\Schema([
332
                        'type'  => 'array',
333
                        'items' => $jsonSchema,
334
                    ]),
335
                ]
336
            ),
337
            'application/xml' => new OASv3\MediaType(
338
                [
339
                    'schema' => new OASv3\Schema([
340
                        'type'  => 'array',
341
                        'items' => $xmlSchema,
342
                        'xml'   => new OASv3\Xml(['name' => 'response']),
343
                    ]),
344
                ]
345
            ),
346
        ];
347
    }
348
349
    /**
350
     * Determine if the operation is a read or a write.
351
     *
352
     * @param string $operation
353
     * @return string
354
     */
355
    private function determineReadWrite(string $operation): string
356
    {
357
        if ($operation === 'post' || $operation === 'put') {
358
            return 'write';
359
        }
360
361
        return 'read';
362
    }
363
364
    /**
365
     * Returns all paths of an api resource without an id in the url.
366
     *
367
     * @param string $apiResourceClass
368
     * @param string $resourceName
369
     * @return OASv3\PathItem
370
     */
371
    private function convertAllToPathItem(string $apiResourceClass, string $resourceName): OASv3\PathItem
372
    {
373
        $paths = [];
374
375
        if ($this->allowed($apiResourceClass, 'all')) {
376
            $paths['get'] = new OASv3\Operation(
377
                [
378
                    '200' => new OASv3\Response(
379
                        'Retrieves all instances of ' . $resourceName,
380
                        $this->convertToContentArray($apiResourceClass, 'get'),
381
                        $this->getDefaultHeaders()
382
                    ),
383
                    '401' => new OASv3\Reference('#/components/responses/NotAuthorized'),
384
                    '429' => new OASv3\Reference('#/components/responses/TooManyRequests'),
385
                    '500' => new OASv3\Reference('#/components/responses/InternalError'),
386
                    '502' => new OASv3\Reference('#/components/responses/ServerDependencyError'),
387
                    '503' => new OASv3\Reference('#/components/responses/MaintenanceMode'),
388
                ],
389
                null,
390
                null,
391
                [
392
                    'tags'       => [$resourceName],
393
                    'parameters' => [
394
                        new OASv3\Parameter('page', 'query', 'pagination index counting from 0', ['schema' => new OASv3\Schema(['type' => 'integer', 'minimum' => 0])]),
395
                        new OASv3\Parameter('limit', 'query', 'number of results', ['schema' => new OASv3\Schema(['type' => 'integer', 'minimum' => 1])]),
396
                    ],
397
                ]
398
            );
399
            $metadata = $this->apiResourceMetadataFactory->getMetadata($apiResourceClass);
400
            $retriever = $metadata->hasResourceRetriever() ? $metadata->getResourceRetriever() : null;
401
            if ($retriever instanceof SearchFilterProviderInterface) {
402
                foreach ($retriever->getSearchFilter($metadata)->getAllPrimitiveSearchFilter() as $name => $filter) {
403
                    $schema = $filter->getSchemaForFilter();
404
                    $paths['get']->parameters[] = new OASv3\Parameter(
405
                        $name,
406
                        'query',
407
                        'search filter ' . $name,
408
                        ['schema' => $schema]
409
                    );
410
                }
411
            }
412
        }
413
414
        if ($this->allowed($apiResourceClass, 'post')) {
415
            $paths['post'] = new OASv3\Operation(
416
                [
417
                    '200' => new OASv3\Response(
418
                        'Creates a new instance of ' . $resourceName,
419
                        $this->convertToContent($apiResourceClass, 'get'),
420
                        $this->getDefaultHeaders()
421
                    ),
422
                    '401' => new OASv3\Reference('#/components/responses/NotAuthorized'),
423
                    '415' => new OASv3\Reference('#/components/responses/InvalidFormat'),
424
                    '422' => new OASv3\Reference('#/components/responses/ValidationError'),
425
                    '429' => new OASv3\Reference('#/components/responses/TooManyRequests'),
426
                    '500' => new OASv3\Reference('#/components/responses/InternalError'),
427
                    '502' => new OASv3\Reference('#/components/responses/ServerDependencyError'),
428
                    '503' => new OASv3\Reference('#/components/responses/MaintenanceMode'),
429
                ],
430
                null,
431
                null,
432
                [
433
                    'tags'        => [$resourceName],
434
                    'requestBody' => new OASv3\RequestBody(
435
                        $this->convertToContent($apiResourceClass, 'post'),
436
                        'the resource as JSON to persist',
437
                        true
438
                    ),
439
                ]
440
            );
441
        }
442
443
        return new OASv3\PathItem($paths);
444
    }
445
446
    /**
447
     * Returns all paths of an api resource with an id in the url.
448
     * @param string $apiResourceClass
449
     * @param string $resourceName
450
     * @return OASv3\PathItem
451
     */
452
    private function convertToPathItem(string $apiResourceClass, string $resourceName, string $identifierName): OASv3\PathItem
453
    {
454
        $paths = [
455
            'parameters' => [
456
                new OASv3\Parameter($identifierName, 'path', 'the id of the resource', ['required' => true, 'schema' => new OASv3\Schema(['type' => 'string'])]),
457
            ],
458
        ];
459
        if ($this->allowed($apiResourceClass, 'get')) {
460
            $paths['get'] = new OASv3\Operation(
461
                [
462
                    '200' => new OASv3\Response(
463
                        'Retrieves a single instance of ' . $resourceName,
464
                        $this->convertToContent($apiResourceClass, 'get'),
465
                        $this->getDefaultHeaders()
466
                    ),
467
                    '401' => new OASv3\Reference('#/components/responses/NotAuthorized'),
468
                    '404' => new OASv3\Reference('#/components/responses/NotFound'),
469
                    '429' => new OASv3\Reference('#/components/responses/TooManyRequests'),
470
                    '500' => new OASv3\Reference('#/components/responses/InternalError'),
471
                    '502' => new OASv3\Reference('#/components/responses/ServerDependencyError'),
472
                    '503' => new OASv3\Reference('#/components/responses/MaintenanceMode'),
473
                ],
474
                null,
475
                null,
476
                [
477
                    'tags' => [$resourceName],
478
                ]
479
            );
480
        }
481
        if ($this->allowed($apiResourceClass, 'delete')) {
482
            $paths['delete'] = new OASv3\Operation(
483
                [
484
                    '204' => new OASv3\Response(
485
                        'Deletes a single instance of ' . $resourceName,
486
                        null,
487
                        $this->getDefaultHeaders()
488
                    ),
489
                    '401' => new OASv3\Reference('#/components/responses/NotAuthorized'),
490
                    '404' => new OASv3\Reference('#/components/responses/NotFound'),
491
                    '429' => new OASv3\Reference('#/components/responses/TooManyRequests'),
492
                    '500' => new OASv3\Reference('#/components/responses/InternalError'),
493
                    '502' => new OASv3\Reference('#/components/responses/ServerDependencyError'),
494
                    '503' => new OASv3\Reference('#/components/responses/MaintenanceMode'),
495
                ],
496
                null,
497
                null,
498
                [
499
                    'tags' => [$resourceName],
500
                ]
501
            );
502
        }
503
        if ($this->allowed($apiResourceClass, 'put')) {
504
            $paths['put'] = new OASv3\Operation(
505
                [
506
                    '200' => new OASv3\Response(
507
                        'Retrieves and update a single instance of ' . $resourceName,
508
                        $this->convertToContent($apiResourceClass, 'get'),
509
                        $this->getDefaultHeaders()
510
                    ),
511
                    '401' => new OASv3\Reference('#/components/responses/NotAuthorized'),
512
                    '404' => new OASv3\Reference('#/components/responses/NotFound'),
513
                    '415' => new OASv3\Reference('#/components/responses/InvalidFormat'),
514
                    '422' => new OASv3\Reference('#/components/responses/ValidationError'),
515
                    '429' => new OASv3\Reference('#/components/responses/TooManyRequests'),
516
                    '500' => new OASv3\Reference('#/components/responses/InternalError'),
517
                    '502' => new OASv3\Reference('#/components/responses/ServerDependencyError'),
518
                    '503' => new OASv3\Reference('#/components/responses/MaintenanceMode'),
519
                ],
520
                null,
521
                null,
522
                [
523
                    'tags'        => [$resourceName],
524
                    'requestBody' => new OASv3\RequestBody(
525
                        $this->convertToContent($apiResourceClass, 'put'),
526
                        'the resource as JSON to persist',
527
                        true
528
                    ),
529
                ]
530
            );
531
        }
532
533
        return new OASv3\PathItem($paths);
534
    }
535
536
    /**
537
     * Returns if a specific REST API call is an allowed method.
538
     *
539
     * @param string $apiResourceClass
540
     * @param string $operation
541
     * @return bool
542
     */
543
    private function allowed(string $apiResourceClass, string $operation): bool
544
    {
545
        $metadata = $this->apiResourceMetadataFactory->getMetadata($apiResourceClass);
546
        switch ($operation) {
547
            case 'all':
548
                return $metadata->allowGetAll();
549
            case 'get':
550
                return $metadata->allowGet();
551
            case 'post':
552
                return $metadata->allowPost();
553
            case 'put':
554
                return $metadata->allowPut();
555
            case 'delete':
556
                return $metadata->allowDelete();
557
        }
558
        // @codeCoverageIgnoreStart
559
        return false;
560
        // @codeCoverageIgnoreEnd
561
    }
562
}
563