Swagger   A
last analyzed

Complexity

Total Complexity 26

Size/Duplication

Total Lines 318
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 4
dl 0
loc 318
ccs 0
cts 196
cp 0
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
C getSwaggerSpec() 0 112 12
A getBasicStructure() 0 16 1
A getBasicPathStructure() 0 41 2
A getPathTags() 0 9 2
B getSummary() 0 23 6
A getSchemaRoutes() 0 26 2
1
<?php
2
/**
3
 * Generate swagger conform specs.
4
 */
5
6
namespace Graviton\SwaggerBundle\Service;
7
8
use Graviton\RestBundle\Service\RestUtils;
9
use Graviton\SchemaBundle\Model\SchemaModel;
10
use Graviton\SchemaBundle\SchemaUtils;
11
use Symfony\Component\Routing\Route;
12
13
/**
14
 * A service that generates a swagger conform service spec dynamically.
15
 *
16
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
17
 * @license  https://opensource.org/licenses/MIT MIT License
18
 * @link     http://swisscom.ch
19
 */
20
class Swagger
21
{
22
23
    /**
24
     * @var \Graviton\RestBundle\Service\RestUtils
25
     */
26
    private $restUtils;
27
28
    /**
29
     * @var SchemaModel
30
     */
31
    private $schemaModel;
32
33
    /**
34
     * @var SchemaUtils
35
     */
36
    private $schemaUtils;
37
38
    /**
39
     * @var array
40
     */
41
    private $versionInformation;
42
43
    /**
44
     * Constructor
45
     *
46
     * @param RestUtils   $restUtils          rest utils
47
     * @param SchemaModel $schemaModel        schema model instance
48
     * @param SchemaUtils $schemaUtils        schema utils
49
     * @param array       $versionInformation version information
50
     */
51
    public function __construct(
52
        RestUtils $restUtils,
53
        SchemaModel $schemaModel,
54
        SchemaUtils $schemaUtils,
55
        array $versionInformation
56
    ) {
57
        $this->restUtils = $restUtils;
58
        $this->schemaModel = $schemaModel;
59
        $this->schemaUtils = $schemaUtils;
60
        $this->versionInformation = $versionInformation;
61
    }
62
63
    /**
64
     * Returns the swagger spec as array
65
     *
66
     * @return array Swagger spec
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string|array>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
67
     */
68
    public function getSwaggerSpec()
69
    {
70
        $ret = $this->getBasicStructure();
71
        $routingMap = $this->restUtils->getServiceRoutingMap();
72
        $paths = [];
73
74
        foreach ($routingMap as $contName => $routes) {
75
            list(, $bundle,, $document) = explode('.', $contName);
76
77
            /** @var Route  $route */
78
            foreach ($routes as $routeName => $route) {
79
                $routeMethod = strtolower($route->getMethods()[0]);
80
81
                if ($routeMethod == 'options') {
82
                    continue;
83
                }
84
85
                // skip /schema/ stuff
86
                if (strpos($route->getPath(), '/schema/') !== false) {
87
                    list($pattern, $method, $data) = $this->getSchemaRoutes($route);
88
                    $paths[$pattern][$method] = $data;
89
                    continue;
90
                }
91
92
                /** @var \Graviton\RestBundle\Model\DocumentModel $thisModel */
93
                $thisModel = $this->restUtils->getModelFromRoute($route);
94
                if ($thisModel === false) {
95
                    throw new \LogicException(
96
                        sprintf(
97
                            'Could not resolve route "%s" to model',
98
                            $routeName
99
                        )
100
                    );
101
                }
102
103
                $entityClassName = str_replace('\\', '', get_class($thisModel));
104
105
                $schema = $this->schemaUtils->getModelSchema($entityClassName, $thisModel, false);
106
107
                $ret['definitions'][$entityClassName] = json_decode(
108
                    $this->restUtils->serializeContent($schema),
109
                    true
110
                );
111
112
                $isCollectionRequest = true;
113
                if (in_array('id', array_keys($route->getRequirements())) === true) {
114
                    $isCollectionRequest = false;
115
                }
116
117
                $thisPattern = $route->getPath();
118
                $entityName = ucfirst($document);
119
120
                $thisPath = $this->getBasicPathStructure(
121
                    $isCollectionRequest,
122
                    $entityName,
123
                    $entityClassName,
124
                    (string) $schema->getProperty('id')->getType()
0 ignored issues
show
Bug introduced by
The method getProperty does only exist in Graviton\SchemaBundle\Document\Schema, but not in stdClass.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
125
                );
126
127
                $thisPath['tags'] = $this->getPathTags($route);
128
                $thisPath['operationId'] = $routeName;
129
                $thisPath['summary'] = $this->getSummary($routeMethod, $isCollectionRequest, $entityName);
130
131
                // post body stuff
132
                if ($routeMethod == 'put' || $routeMethod == 'post') {
133
                    // special handling for POST/PUT.. we need to have 2 schemas, one for response, one for request..
134
                    // we don't want to have ID in the request body within those requests do we..
135
                    // an exception is when id is required..
136
                    $incomingEntitySchema = $entityClassName;
137
                    if (is_null($schema->getRequired()) || !in_array('id', $schema->getRequired())) {
0 ignored issues
show
Bug introduced by
The method getRequired does only exist in Graviton\SchemaBundle\Document\Schema, but not in stdClass.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
138
                        $incomingEntitySchema = $incomingEntitySchema . 'Incoming';
139
                        $incomingSchema = clone $schema;
140
                        $incomingSchema->removeProperty('id');
0 ignored issues
show
Bug introduced by
The method removeProperty does only exist in Graviton\SchemaBundle\Document\Schema, but not in stdClass.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
141
                        $ret['definitions'][$incomingEntitySchema] = json_decode(
142
                            $this->restUtils->serializeContent($incomingSchema),
143
                            true
144
                        );
145
                    }
146
147
                    $thisPath['parameters'][] = array(
148
                        'name' => $bundle,
149
                        'in' => 'body',
150
                        'description' => 'Post',
151
                        'required' => true,
152
                        'schema' => array('$ref' => '#/definitions/' . $incomingEntitySchema)
153
                    );
154
155
                    if ($routeMethod == 'post') {
156
                        $thisPath['responses'][201] = $thisPath['responses'][200];
157
                        unset($thisPath['responses'][200]);
158
                    }
159
160
                    // add error responses..
161
                    $thisPath['responses'][400] = array(
162
                        'description' => 'Bad request',
163
                        'schema' => array(
164
                            'type' => 'object'
165
                        )
166
                    );
167
                }
168
169
                $paths[$thisPattern][$routeMethod] = $thisPath;
170
            }
171
        }
172
173
        $ret['definitions']['SchemaModel'] = $this->schemaModel->getSchema();
174
175
        ksort($paths);
176
        $ret['paths'] = $paths;
177
178
        return $ret;
179
    }
180
181
    /**
182
     * Basic structure of the spec
183
     *
184
     * @return array Basic structure
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string|array|string[]>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
185
     */
186
    private function getBasicStructure()
187
    {
188
        $ret = [];
189
        $ret['swagger'] = '2.0';
190
191
        $ret['info'] = array(
192
            'version' => $this->versionInformation['self'],
193
            'title' => 'Graviton REST Services',
194
            'description' => 'Testable API Documentation of this Graviton instance.',
195
        );
196
197
        $ret['basePath'] = '/';
198
        $ret['schemes'] = array('http', 'https');
199
200
        return $ret;
201
    }
202
203
    /**
204
     * Return the basic structure of a path element
205
     *
206
     * @param bool   $isCollectionRequest if collection request
207
     * @param string $entityName          entity name
208
     * @param string $entityClassName     class name
209
     * @param string $idType              type of id field
210
     *
211
     * @return array Path spec
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array<strin...array<string,string>>>>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
212
     */
213
    protected function getBasicPathStructure($isCollectionRequest, $entityName, $entityClassName, $idType)
214
    {
215
        $thisPath = array(
216
            'consumes' => array('application/json'),
217
            'produces' => array('application/json')
218
        );
219
220
        // collection return or not?
221
        if (!$isCollectionRequest) {
222
            // add object response
223
            $thisPath['responses'] = array(
224
                200 => array(
225
                    'description' => $entityName . ' response',
226
                    'schema' => array('$ref' => '#/definitions/' . $entityClassName)
227
                ),
228
                404 => array(
229
                    'description' => 'Resource not found'
230
                )
231
            );
232
233
            // add id param
234
            $thisPath['parameters'][] = array(
235
                'name' => 'id',
236
                'in' => 'path',
237
                'description' => 'ID of ' . $entityName . ' item to fetch/update',
238
                'required' => true,
239
                'type' => $idType
240
            );
241
        } else {
242
            // add array response
243
            $thisPath['responses'][200] = array(
244
                'description' => $entityName . ' response',
245
                'schema' => array(
246
                    'type' => 'array',
247
                    'items' => array('$ref' => '#/definitions/' . $entityClassName)
248
                )
249
            );
250
        }
251
252
        return $thisPath;
253
    }
254
255
    /**
256
     * Returns the tags (which influences the grouping visually) for a given route
257
     *
258
     * @param Route $route route
259
     * @param int   $part  part of route to use for generating a tag
260
     *
261
     * @return array Array of tags..
262
     */
263
    protected function getPathTags(Route $route, $part = 1)
264
    {
265
        $ret = [];
266
        $routeParts = explode('/', $route->getPath());
267
        if (isset($routeParts[$part])) {
268
            $ret[] = ucfirst($routeParts[$part]);
269
        }
270
        return $ret;
271
    }
272
273
    /**
274
     * Returns a meaningful summary depending on certain conditions
275
     *
276
     * @param string $method              Method
277
     * @param bool   $isCollectionRequest If collection request
278
     * @param string $entityName          Name of entity
279
     *
280
     * @return string summary
281
     */
282
    protected function getSummary($method, $isCollectionRequest, $entityName)
283
    {
284
        $ret = '';
285
        // meaningful descriptions..
286
        switch ($method) {
287
            case 'get':
288
                if ($isCollectionRequest) {
289
                    $ret = 'Get collection of ' . $entityName . ' resources';
290
                } else {
291
                    $ret = 'Get single ' . $entityName . ' resources';
292
                }
293
                break;
294
            case 'post':
295
                $ret = 'Create new ' . $entityName . ' resource';
296
                break;
297
            case 'put':
298
                $ret = 'Update existing ' . $entityName . ' resource';
299
                break;
300
            case 'delete':
301
                $ret = 'Delete existing ' . $entityName . ' resource';
302
        }
303
        return $ret;
304
    }
305
306
    /**
307
     * @param Route $route route
308
     *
309
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string|array<strin...ring>>[]|array|string>>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
310
     */
311
    protected function getSchemaRoutes(Route $route)
312
    {
313
        $path = $route->getPath();
314
315
        $describedService = substr(substr($path, 7), 0, substr($path, -5) == '/item' ? -7 : -10);
316
317
        $tags = array_merge(['Schema'], $this->getPathTags($route, 2));
318
319
        return [
320
            $path,
321
            'get',
322
            [
323
                'produces' => [
324
                    'application/json',
325
                ],
326
                'responses' => [
327
                    200 => [
328
                        'description' => 'JSON-Schema for ' . $describedService . '.',
329
                        'schema' => ['$ref' => '#/definitions/SchemaModel'],
330
                    ]
331
                ],
332
                'tags' => $tags,
333
                'summary' => 'Get schema information for ' . $describedService . ' endpoints.',
334
            ]
335
        ];
336
    }
337
}
338