Completed
Push — master ( f9d007...4fb519 )
by Narcotic
68:19 queued 60:57
created

Swagger   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 318
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 4.76%

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 5
dl 0
loc 318
ccs 6
cts 126
cp 0.0476
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
A getBasicStructure() 0 16 1
B getBasicPathStructure() 0 41 2
A getPathTags() 0 9 2
B getSchemaRoutes() 0 26 2
C getSwaggerSpec() 0 112 12
B getSummary() 0 23 6
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 2
    public function __construct(
55
        RestUtils $restUtils,
56
        SchemaModel $schemaModel,
57
        SchemaUtils $schemaUtils,
58
        CoreUtils $coreUtils
59
    ) {
60 2
        $this->restUtils = $restUtils;
61 2
        $this->schemaModel = $schemaModel;
62 2
        $this->schemaUtils = $schemaUtils;
63 2
        $this->coreUtils = $coreUtils;
64 2
    }
65
66
    /**
67
     * Returns the swagger spec as array
68
     *
69
     * @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...
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->getPath();
121
                $entityName = ucfirst($document);
122
123
                $thisPath = $this->getBasicPathStructure(
124
                    $isCollectionRequest,
125
                    $entityName,
126
                    $entityClassName,
127
                    (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...
128
                );
129
130
                $thisPath['tags'] = $this->getPathTags($route);
131
                $thisPath['operationId'] = $routeName;
132
                $thisPath['summary'] = $this->getSummary($routeMethod, $isCollectionRequest, $entityName);
133
134
                // post body stuff
135
                if ($routeMethod == 'put' || $routeMethod == 'post') {
136
                    // special handling for POST/PUT.. we need to have 2 schemas, one for response, one for request..
137
                    // we don't want to have ID in the request body within those requests do we..
138
                    // an exception is when id is required..
139
                    $incomingEntitySchema = $entityClassName;
140
                    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...
141
                        $incomingEntitySchema = $incomingEntitySchema . 'Incoming';
142
                        $incomingSchema = clone $schema;
143
                        $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...
144
                        $ret['definitions'][$incomingEntitySchema] = json_decode(
145
                            $this->restUtils->serializeContent($incomingSchema),
146
                            true
147
                        );
148
                    }
149
150
                    $thisPath['parameters'][] = array(
151
                        'name' => $bundle,
152
                        'in' => 'body',
153
                        'description' => 'Post',
154
                        'required' => true,
155
                        'schema' => array('$ref' => '#/definitions/' . $incomingEntitySchema)
156
                    );
157
158
                    if ($routeMethod == 'post') {
159
                        $thisPath['responses'][201] = $thisPath['responses'][200];
160
                        unset($thisPath['responses'][200]);
161
                    }
162
163
                    // add error responses..
164
                    $thisPath['responses'][400] = array(
165
                        'description' => 'Bad request',
166
                        'schema' => array(
167
                            'type' => 'object'
168
                        )
169
                    );
170
                }
171
172
                $paths[$thisPattern][$routeMethod] = $thisPath;
173
            }
174
        }
175
176
        $ret['definitions']['SchemaModel'] = $this->schemaModel->getSchema();
177
178
        ksort($paths);
179
        $ret['paths'] = $paths;
180
181
        return $ret;
182
    }
183
184
    /**
185
     * Basic structure of the spec
186
     *
187
     * @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...
188
     */
189
    private function getBasicStructure()
190
    {
191
        $ret = array();
192
        $ret['swagger'] = '2.0';
193
194
        $ret['info'] = array(
195
            'version' => $this->coreUtils->getWrapperVersion()['version'],
196
            'title' => 'Graviton REST Services',
197
            'description' => 'Testable API Documentation of this Graviton instance.',
198
        );
199
200
        $ret['basePath'] = '/';
201
        $ret['schemes'] = array('http', 'https');
202
203
        return $ret;
204
    }
205
206
    /**
207
     * Return the basic structure of a path element
208
     *
209
     * @param bool   $isCollectionRequest if collection request
210
     * @param string $entityName          entity name
211
     * @param string $entityClassName     class name
212
     * @param string $idType              type of id field
213
     *
214
     * @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...
215
     */
216
    protected function getBasicPathStructure($isCollectionRequest, $entityName, $entityClassName, $idType)
217
    {
218
        $thisPath = array(
219
            'consumes' => array('application/json'),
220
            'produces' => array('application/json')
221
        );
222
223
        // collection return or not?
224
        if (!$isCollectionRequest) {
225
            // add object response
226
            $thisPath['responses'] = array(
227
                200 => array(
228
                    'description' => $entityName . ' response',
229
                    'schema' => array('$ref' => '#/definitions/' . $entityClassName)
230
                ),
231
                404 => array(
232
                    'description' => 'Resource not found'
233
                )
234
            );
235
236
            // add id param
237
            $thisPath['parameters'][] = array(
238
                'name' => 'id',
239
                'in' => 'path',
240
                'description' => 'ID of ' . $entityName . ' item to fetch/update',
241
                'required' => true,
242
                'type' => $idType
243
            );
244
        } else {
245
            // add array response
246
            $thisPath['responses'][200] = array(
247
                'description' => $entityName . ' response',
248
                'schema' => array(
249
                    'type' => 'array',
250
                    'items' => array('$ref' => '#/definitions/' . $entityClassName)
251
                )
252
            );
253
        }
254
255
        return $thisPath;
256
    }
257
258
    /**
259
     * Returns the tags (which influences the grouping visually) for a given route
260
     *
261
     * @param Route $route route
262
     * @param int   $part  part of route to use for generating a tag
263
     *
264
     * @return array Array of tags..
265
     */
266
    protected function getPathTags(Route $route, $part = 1)
267
    {
268
        $ret = array();
269
        $routeParts = explode('/', $route->getPath());
270
        if (isset($routeParts[$part])) {
271
            $ret[] = ucfirst($routeParts[$part]);
272
        }
273
        return $ret;
274
    }
275
276
    /**
277
     * Returns a meaningful summary depending on certain conditions
278
     *
279
     * @param string $method              Method
280
     * @param bool   $isCollectionRequest If collection request
281
     * @param string $entityName          Name of entity
282
     *
283
     * @return string summary
284
     */
285
    protected function getSummary($method, $isCollectionRequest, $entityName)
286
    {
287
        $ret = '';
288
        // meaningful descriptions..
289
        switch ($method) {
290
            case 'get':
291
                if ($isCollectionRequest) {
292
                    $ret = 'Get collection of ' . $entityName . ' resources';
293
                } else {
294
                    $ret = 'Get single ' . $entityName . ' resources';
295
                }
296
                break;
297
            case 'post':
298
                $ret = 'Create new ' . $entityName . ' resource';
299
                break;
300
            case 'put':
301
                $ret = 'Update existing ' . $entityName . ' resource';
302
                break;
303
            case 'delete':
304
                $ret = 'Delete existing ' . $entityName . ' resource';
305
        }
306
        return $ret;
307
    }
308
309
    /**
310
     * @param Route $route route
311
     *
312
     * @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...
313
     */
314
    protected function getSchemaRoutes(Route $route)
315
    {
316
        $path = $route->getPath();
317
318
        $describedService = substr(substr($path, 7), 0, substr($path, -5) == '/item' ? -7 : -10);
319
320
        $tags = array_merge(['Schema'], $this->getPathTags($route, 2));
321
322
        return [
323
            $path,
324
            'get',
325
            [
326
                'produces' => [
327
                    'application/json',
328
                ],
329
                'responses' => [
330
                    200 => [
331
                        'description' => 'JSON-Schema for ' . $describedService . '.',
332
                        'schema' => ['$ref' => '#/definitions/SchemaModel'],
333
                    ]
334
                ],
335
                'tags' => $tags,
336
                'summary' => 'Get schema information for ' . $describedService . ' endpoints.',
337
            ]
338
        ];
339
    }
340
}
341