Completed
Push — feature/OPTIONS_4_sf28_update ( 830dee...692213 )
by
unknown
09:05
created

Swagger::getSwaggerSpec()   D

Complexity

Conditions 13
Paths 25

Size

Total Lines 115
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 115
rs 4.9922
cc 13
eloc 67
nc 25
nop 0

How to fix   Long Method    Complexity   

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
 * Generate swagger conform specs.
4
 */
5
6
namespace Graviton\SwaggerBundle\Service;
7
8
use Graviton\CoreBundle\Service\CoreUtils;
9
use Graviton\ExceptionBundle\Exception\MalformedInputException;
10
use Graviton\RestBundle\Service\RestUtils;
11
use Graviton\SchemaBundle\Model\SchemaModel;
12
use Graviton\SchemaBundle\SchemaUtils;
13
use Symfony\Component\Config\Definition\Exception\Exception;
14
use Symfony\Component\Routing\Route;
15
16
/**
17
 * A service that generates a swagger conform service spec dynamically.
18
 *
19
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
20
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
21
 * @link     http://swisscom.ch
22
 */
23
class Swagger
24
{
25
26
    /**
27
     * @var \Graviton\RestBundle\Service\RestUtils
28
     */
29
    private $restUtils;
30
31
    /**
32
     * @var SchemaModel
33
     */
34
    private $schemaModel;
35
36
    /**
37
     * @var SchemaUtils
38
     */
39
    private $schemaUtils;
40
41
    /**
42
     * @var CoreUtils
43
     */
44
    private $coreUtils;
45
46
    /**
47
     * Constructor
48
     *
49
     * @param RestUtils   $restUtils   rest utils
50
     * @param SchemaModel $schemaModel schema model instance
51
     * @param SchemaUtils $schemaUtils schema utils
52
     * @param CoreUtils   $coreUtils   coreUtils
53
     */
54
    public function __construct(
55
        RestUtils $restUtils,
56
        SchemaModel $schemaModel,
57
        SchemaUtils $schemaUtils,
58
        CoreUtils $coreUtils
59
    ) {
60
        $this->restUtils = $restUtils;
61
        $this->schemaModel = $schemaModel;
62
        $this->schemaUtils = $schemaUtils;
63
        $this->coreUtils = $coreUtils;
64
    }
65
66
    /**
67
     * Returns the swagger spec as array
68
     *
69
     * @return array Swagger spec
70
     */
71
    public function getSwaggerSpec()
72
    {
73
        $ret = $this->getBasicStructure();
74
        $routingMap = $this->restUtils->getServiceRoutingMap();
75
        $paths = array();
76
77
        foreach ($routingMap as $contName => $routes) {
78
            list(, $bundle,, $document) = explode('.', $contName);
79
80
            /** @var Route  $route */
81
            foreach ($routes as $routeName => $route) {
82
                $routeMethod = strtolower($route->getMethods()[0]);
83
84
                if ($routeMethod == 'options') {
85
                    continue;
86
                }
87
88
                // skip /schema/ stuff
89
                if (strpos($route->getPath(), '/schema/') !== false) {
90
                    list($pattern, $method, $data) = $this->getSchemaRoutes($route);
91
                    $paths[$pattern][$method] = $data;
92
                    continue;
93
                }
94
95
                /** @var \Graviton\RestBundle\Model\DocumentModel $thisModel */
96
                $thisModel = $this->restUtils->getModelFromRoute($route);
97
                if ($thisModel === false) {
98
                    throw new \LogicException(
99
                        sprintf(
100
                            'Could not resolve route "%s" to model',
101
                            $routeName
102
                        )
103
                    );
104
                }
105
106
                $entityClassName = str_replace('\\', '', get_class($thisModel));
107
108
                $schema = $this->schemaUtils->getModelSchema($entityClassName, $thisModel, false);
109
110
                $ret['definitions'][$entityClassName] = json_decode(
111
                    $this->restUtils->serializeContent($schema),
112
                    true
113
                );
114
115
                $isCollectionRequest = true;
116
                if (in_array('id', array_keys($route->getRequirements())) === true) {
117
                    $isCollectionRequest = false;
118
                }
119
120
                $thisPattern = $route->getPattern();
121
                $entityName = ucfirst($document);
122
123
                $type = method_exists($schema->getProperty('id'), 'getType') ?
124
                    $schema->getProperty('id')->getType() : 'local';
125
126
                $thisPath = $this->getBasicPathStructure(
127
                    $isCollectionRequest,
128
                    $entityName,
129
                    $entityClassName,
130
                    $type
131
                );
132
133
                $thisPath['tags'] = $this->getPathTags($route);
134
                $thisPath['operationId'] = $routeName;
135
                $thisPath['summary'] = $this->getSummary($routeMethod, $isCollectionRequest, $entityName);
136
137
                // post body stuff
138
                if ($routeMethod == 'put' || $routeMethod == 'post') {
139
                    // special handling for POST/PUT.. we need to have 2 schemas, one for response, one for request..
140
                    // we don't want to have ID in the request body within those requests do we..
141
                    // an exception is when id is required..
142
                    $incomingEntitySchema = $entityClassName;
143
                    if (is_null($schema->getRequired()) || !in_array('id', $schema->getRequired())) {
144
                        $incomingEntitySchema = $incomingEntitySchema . 'Incoming';
145
                        $incomingSchema = clone $schema;
146
                        $incomingSchema->removeProperty('id');
147
                        $ret['definitions'][$incomingEntitySchema] = json_decode(
148
                            $this->restUtils->serializeContent($incomingSchema),
149
                            true
150
                        );
151
                    }
152
153
                    $thisPath['parameters'][] = array(
154
                        'name' => $bundle,
155
                        'in' => 'body',
156
                        'description' => 'Post',
157
                        'required' => true,
158
                        'schema' => array('$ref' => '#/definitions/' . $incomingEntitySchema)
159
                    );
160
161
                    if ($routeMethod == 'post') {
162
                        $thisPath['responses'][201] = $thisPath['responses'][200];
163
                        unset($thisPath['responses'][200]);
164
                    }
165
166
                    // add error responses..
167
                    $thisPath['responses'][400] = array(
168
                        'description' => 'Bad request',
169
                        'schema' => array(
170
                            'type' => 'object'
171
                        )
172
                    );
173
                }
174
175
                $paths[$thisPattern][$routeMethod] = $thisPath;
176
            }
177
        }
178
179
        $ret['definitions']['SchemaModel'] = $this->schemaModel->getSchema();
180
181
        ksort($paths);
182
        $ret['paths'] = $paths;
183
184
        return $ret;
185
    }
186
187
    /**
188
     * Basic structure of the spec
189
     *
190
     * @return array Basic structure
191
     */
192
    private function getBasicStructure()
193
    {
194
        $ret = array();
195
        $ret['swagger'] = '2.0';
196
197
        $ret['info'] = array(
198
            'version' => $this->coreUtils->getWrapperVersion()['version'],
199
            'title' => 'Graviton REST Services',
200
            'description' => 'Testable API Documentation of this Graviton instance.',
201
        );
202
203
        $ret['basePath'] = '/';
204
        $ret['schemes'] = array('http', 'https');
205
206
        return $ret;
207
    }
208
209
    /**
210
     * Return the basic structure of a path element
211
     *
212
     * @param bool   $isCollectionRequest if collection request
213
     * @param string $entityName          entity name
214
     * @param string $entityClassName     class name
215
     * @param string $idType              type of id field
216
     *
217
     * @return array Path spec
218
     */
219
    protected function getBasicPathStructure($isCollectionRequest, $entityName, $entityClassName, $idType)
220
    {
221
        $thisPath = array(
222
            'consumes' => array('application/json'),
223
            'produces' => array('application/json')
224
        );
225
226
        // collection return or not?
227
        if (!$isCollectionRequest) {
228
            // add object response
229
            $thisPath['responses'] = array(
230
                200 => array(
231
                    'description' => $entityName . ' response',
232
                    'schema' => array('$ref' => '#/definitions/' . $entityClassName)
233
                ),
234
                404 => array(
235
                    'description' => 'Resource not found'
236
                )
237
            );
238
239
            // add id param
240
            $thisPath['parameters'][] = array(
241
                'name' => 'id',
242
                'in' => 'path',
243
                'description' => 'ID of ' . $entityName . ' item to fetch/update',
244
                'required' => true,
245
                'type' => $idType
246
            );
247
        } else {
248
            // add array response
249
            $thisPath['responses'][200] = array(
250
                'description' => $entityName . ' response',
251
                'schema' => array(
252
                    'type' => 'array',
253
                    'items' => array('$ref' => '#/definitions/' . $entityClassName)
254
                )
255
            );
256
        }
257
258
        return $thisPath;
259
    }
260
261
    /**
262
     * Returns the tags (which influences the grouping visually) for a given route
263
     *
264
     * @param Route $route route
265
     * @param int   $part  part of route to use for generating a tag
266
     *
267
     * @return array Array of tags..
268
     */
269
    protected function getPathTags(Route $route, $part = 1)
270
    {
271
        $ret = array();
272
        $routeParts = explode('/', $route->getPath());
273
        if (isset($routeParts[$part])) {
274
            $ret[] = ucfirst($routeParts[$part]);
275
        }
276
        return $ret;
277
    }
278
279
    /**
280
     * Returns a meaningful summary depending on certain conditions
281
     *
282
     * @param string $method              Method
283
     * @param bool   $isCollectionRequest If collection request
284
     * @param string $entityName          Name of entity
285
     *
286
     * @return string summary
287
     */
288
    protected function getSummary($method, $isCollectionRequest, $entityName)
289
    {
290
        $ret = '';
291
        // meaningful descriptions..
292
        switch ($method) {
293
            case 'get':
294
                if ($isCollectionRequest) {
295
                    $ret = 'Get collection of ' . $entityName . ' resources';
296
                } else {
297
                    $ret = 'Get single ' . $entityName . ' resources';
298
                }
299
                break;
300
            case 'post':
301
                $ret = 'Create new ' . $entityName . ' resource';
302
                break;
303
            case 'put':
304
                $ret = 'Update existing ' . $entityName . ' resource';
305
                break;
306
            case 'delete':
307
                $ret = 'Delete existing ' . $entityName . ' resource';
308
        }
309
        return $ret;
310
    }
311
312
    /**
313
     * @param Route $route route
314
     *
315
     * @return array
316
     */
317
    protected function getSchemaRoutes(Route $route)
318
    {
319
        $path = $route->getPath();
320
321
        $describedService = substr(substr($path, 7), 0, substr($path, -5) == '/item' ? -7 : -10);
322
323
        $tags = array_merge(['Schema'], $this->getPathTags($route, 2));
324
325
        return [
326
            $path,
327
            'get',
328
            [
329
                'produces' => [
330
                    'application/json',
331
                ],
332
                'responses' => [
333
                    200 => [
334
                        'description' => 'JSON-Schema for ' . $describedService . '.',
335
                        'schema' => ['$ref' => '#/definitions/SchemaModel'],
336
                    ]
337
                ],
338
                'tags' => $tags,
339
                'summary' => 'Get schema information for ' . $describedService . ' endpoints.',
340
            ]
341
        ];
342
    }
343
}
344