Passed
Push — master ( 70f98a...9ce8e2 )
by Fran
05:26
created

DocumentorService::extractSwaggerDefinition()   C

Complexity

Conditions 7
Paths 2

Size

Total Lines 39
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 27
nc 2
nop 1
dl 0
loc 39
rs 6.7272
c 0
b 0
f 0
1
<?php
2
    namespace PSFS\services;
3
4
    use PSFS\base\Logger;
5
    use PSFS\base\Service;
6
    use PSFS\base\Singleton;
7
    use Symfony\Component\Finder\Finder;
8
9
    /**
10
     * Class DocumentorService
11
     * @package PSFS\services
12
     */
13
    class DocumentorService extends Service
14
    {
15
        const DTO_INTERFACE = '\\PSFS\\base\\dto\\Dto';
16
        const MODEL_INTERFACE = '\\Propel\\Runtime\\ActiveRecord\\ActiveRecordInterface';
17
        /**
18
         * @Inyectable
19
         * @var \PSFS\base\Router route
20
         */
21
        protected $route;
22
23
        /**
24
         * Method that extract all modules
25
         * @return array
26
         */
27
        public function getModules()
28
        {
29
            $modules = [];
30
            $domains = $this->route->getDomains();
31
            if (count($domains)) {
32
                foreach (array_keys($domains) as $domain) {
33
                    try {
34
                        if (!preg_match('/^\@ROOT/i', $domain)) {
35
                            $modules[] = str_replace('/', '', str_replace('@', '', $domain));
36
                        }
37
                    } catch (\Exception $e) {
38
                        $modules[] = $e->getMessage();
39
                    }
40
                }
41
            }
42
43
            return $modules;
44
        }
45
46
        /**
47
         * Method that extract all endpoints for each module
48
         *
49
         * @param string $module
50
         *
51
         * @return array
52
         */
53
        public function extractApiEndpoints($module)
54
        {
55
            $module_path = CORE_DIR . DIRECTORY_SEPARATOR . $module . DIRECTORY_SEPARATOR . "Api";
56
            $endpoints = [];
57
            if (file_exists($module_path)) {
58
                $finder = new Finder();
59
                $finder->files()->depth('== 0')->in($module_path)->name('*.php');
60
                if (count($finder)) {
61
                    /** @var \SplFileInfo $file */
62
                    foreach ($finder as $file) {
63
                        $namespace = "\\{$module}\\Api\\" . str_replace('.php', '', $file->getFilename());
64
                        $endpoints[$namespace] = $this->extractApiInfo($namespace);
65
                    }
66
                }
67
            }
68
69
            return $endpoints;
70
        }
71
72
        /**
73
         * Method that extract all the endpoit information by reflection
74
         *
75
         * @param string $namespace
76
         *
77
         * @return array
78
         */
79
        public function extractApiInfo($namespace)
80
        {
81
            $info = [];
82
            $reflection = new \ReflectionClass($namespace);
83
            $publicMethods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
84
            if (count($publicMethods)) {
85
                /** @var \ReflectionMethod $method */
86
                foreach ($publicMethods as $method) {
87
                    try {
88
                        $mInfo = $this->extractMethodInfo($namespace, $method, $reflection);
89
                        if (NULL !== $mInfo) {
90
                            $info[] = $mInfo;
91
                        }
92
                    } catch (\Exception $e) {
93
                        Logger::getInstance()->errorLog($e->getMessage());
94
                    }
95
                }
96
            }
97
98
            return $info;
99
        }
100
101
        /**
102
         * Extract route from doc comments
103
         *
104
         * @param string $comments
105
         *
106
         * @return string
107
         */
108
        protected function extractRoute($comments = '')
109
        {
110
            $route = '';
111
            preg_match('/@route\ (.*)\n/i', $comments, $route);
112
113
            return $route[1];
114
        }
115
116
        /**
117
         * Extract method from doc comments
118
         *
119
         * @param string $comments
120
         *
121
         * @return string
122
         */
123
        protected function extractMethod($comments = '')
124
        {
125
            $method = 'GET';
126
            preg_match('/@(get|post|put|delete)\n/i', $comments, $method);
127
128
            return strtoupper($method[1]);
129
        }
130
131
        /**
132
         * Extract visibility from doc comments
133
         *
134
         * @param string $comments
135
         *
136
         * @return boolean
137
         */
138
        protected function extractVisibility($comments = '')
139
        {
140
            $visible = TRUE;
141
            preg_match('/@visible\ (true|false)\n/i', $comments, $visibility);
142
            if (count($visibility)) {
143
                $visible = !('false' == $visibility[1]);
144
            }
145
146
            return $visible;
147
        }
148
149
        /**
150
         * Method that extract the description for the endpoint
151
         *
152
         * @param string $comments
153
         *
154
         * @return string
155
         */
156
        protected function extractDescription($comments = '')
157
        {
158
            $description = '';
159
            $docs = explode("\n", $comments);
160
            if (count($docs)) {
161
                foreach ($docs as &$doc) {
162 View Code Duplication
                    if (!preg_match('/(\*\*|\@)/i', $doc) && preg_match('/\*\ /i', $doc)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
163
                        $doc = explode('* ', $doc);
164
                        $description = $doc[1];
165
                    }
166
                }
167
            }
168
169
            return $description;
170
        }
171
172
        /**
173
         * Method that extract the type of a variable
174
         *
175
         * @param string $comments
176
         *
177
         * @return string
178
         */
179
        protected function extractVarType($comments = '')
180
        {
181
            $type = 'string';
182
            preg_match('/@var\ (.*) (.*)\n/i', $comments, $varType);
183
            if (count($varType)) {
184
                $aux = trim($varType[1]);
185
                $type = str_replace(' ', '', strlen($aux) > 0 ? $varType[1] : $varType[2]);
186
            }
187
188
            return $type;
189
        }
190
191
        /**
192
         * Method that extract the payload for the endpoint
193
         *
194
         * @param string $model
195
         * @param string $comments
196
         *
197
         * @return array
198
         */
199
        protected function extractPayload($model, $comments = '')
200
        {
201
            $payload = [];
202
            preg_match('/@payload\ (.*)\n/i', $comments, $doc);
203
            if (count($doc)) {
204
                $namespace = str_replace('{__API__}', $model, $doc[1]);
205
                $payload = $this->extractModelFields($namespace);
206
            }
207
208
            return $payload;
209
        }
210
211
        /**
212
         * Extract all the properties from Dto class
213
         *
214
         * @param string $class
215
         *
216
         * @return array
217
         */
218
        protected function extractDtoProperties($class)
219
        {
220
            $properties = [];
221
            $reflector = new \ReflectionClass($class);
222
            if ($reflector->isSubclassOf(self::DTO_INTERFACE)) {
223
                Singleton::extractProperties($reflector, $properties, \ReflectionMethod::IS_PUBLIC);
224
            }
225
226
            return $properties;
227
        }
228
229
        /**
230
         * Extract return class for api endpoint
231
         *
232
         * @param string $model
233
         * @param string $comments
234
         *
235
         * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be array? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
236
         */
237
        protected function extractReturn($model, $comments = '')
238
        {
239
            $modelDto  = [];
240
            preg_match('/\@return\ (.*)\((.*)\)\n/i', $comments, $returnTypes);
241
            if (count($returnTypes)) {
242
                // Extract principal DTO information
243
                if (array_key_exists(1, $returnTypes)) {
244
                    $modelDto = $this->extractDtoProperties($returnTypes[1]);
245
                }
246
                if (array_key_exists(2, $returnTypes)) {
247
                    $subDtos = preg_split('/,?\ /', str_replace('{__API__}', $model, $returnTypes[2]));
248
                    if (count($subDtos)) {
249
                        foreach ($subDtos as $subDto) {
250
                            $isArray = false;
251
                            list($field, $dto) = explode('=', $subDto);
252
                            if (false !== strpos($dto, '[') && false !== strpos($dto, ']')) {
253
                                $dto = str_replace(']', '', str_replace('[', '', $dto));
254
                                $isArray = true;
255
                            }
256
                            $dto = $this->extractModelFields($dto);
257
                            $modelDto[$field] = ($isArray) ? [$dto] : $dto;
258
                        }
259
                    }
260
                }
261
            }
262
263
            return $modelDto;
264
        }
265
266
        /**
267
         * Extract all fields from a ActiveResource model
268
         *
269
         * @param string $namespace
270
         *
271
         * @return mixed
272
         */
273
        protected function extractModelFields($namespace)
274
        {
275
            $payload = [];
276
            try {
277
                $reflector = new \ReflectionClass($namespace);
278
                // Checks if reflector is a subclass of propel ActiveRecords
279
                if (NULL !== $reflector && $reflector->isSubclassOf(self::MODEL_INTERFACE)) {
280
                    $tableMap = $namespace::TABLE_MAP;
281
                    $fieldNames = $tableMap::getFieldNames();
282
                    if (count($fieldNames)) {
283
                        foreach ($fieldNames as $field) {
284
                            $variable = $reflector->getProperty(strtolower($field));
285
                            $varDoc = $variable->getDocComment();
286
                            $payload[$field] = $this->extractVarType($varDoc);
287
                        }
288
                    }
289
                } elseif (null !== $reflector && $reflector->isSubclassOf(self::DTO_INTERFACE)) {
290
                    $payload = $this->extractDtoProperties($namespace);
291
                }
292
            } catch (\Exception $e) {
293
                Logger::getInstance()->errorLog($e->getMessage());
294
            }
295
296
            return $payload;
297
        }
298
299
        /**
300
         * Method that extract all the needed info for each method in each API
301
         *
302
         * @param string $namespace
303
         * @param \ReflectionMethod $method
304
         * @param \ReflectionClass $reflection
305
         *
306
         * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be array<string,string|array>|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
307
         */
308
        protected function extractMethodInfo($namespace, \ReflectionMethod $method, \ReflectionClass $reflection)
309
        {
310
            $methodInfo = NULL;
311
            $docComments = $method->getDocComment();
312
            $shortName = $reflection->getShortName();
313
            $modelNamespace = str_replace('Api', 'Models', $namespace);
314
            if (FALSE !== $docComments && preg_match('/\@route\ /i', $docComments)) {
315
                $visibility = $this->extractVisibility($docComments);
316
                $route = str_replace('{__API__}', $shortName, $this->extractRoute($docComments));
317
                if ($visibility && preg_match('/^\/api\//i', $route)) {
318
                    try {
319
                        $methodInfo = [
320
                            'url'         => $route,
321
                            'method'      => $this->extractMethod($docComments),
322
                            'description' => str_replace('{__API__}', $shortName, $this->extractDescription($docComments)),
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 123 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
323
                            'return'      => $this->extractReturn($modelNamespace, $docComments),
324
                        ];
325
                        if (in_array($methodInfo['method'], ['POST', 'PUT'])) {
326
                            $methodInfo['payload'] = $this->extractPayload($modelNamespace, $docComments);
327
                        }
328
                    } catch (\Exception $e) {
329
                        jpre($e->getMessage());
330
                        Logger::getInstance()->errorLog($e->getMessage());
331
                    }
332
                }
333
            }
334
335
            return $methodInfo;
336
        }
337
338
        /**
339
         * Translator from php types to swagger types
340
         * @param string $format
341
         *
342
         * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use 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...
343
         */
344
        public static function translateSwaggerFormats($format)
345
        {
346
            switch(strtolower($format)) {
347
                case 'bool':
348
                case 'boolean':
349
                    $swaggerType = 'boolean';
350
                    $swaggerFormat = '';
351
                    break;
352
                default:
353
                case 'string':
354
                case 'varchar':
355
                    $swaggerType = 'string';
356
                    $swaggerFormat = '';
357
                    break;
358
                case 'binary':
359
                case 'varbinary':
360
                    $swaggerType = 'string';
361
                    $swaggerFormat = 'binary';
362
                    break;
363
                case 'int':
364
                case 'integer':
365
                case 'float':
366
                case 'double':
367
                    $swaggerType = 'integer';
368
                    $swaggerFormat = 'int32';
369
                    break;
370
                case 'date':
371
                    $swaggerType = 'string';
372
                    $swaggerFormat = 'date';
373
                    break;
374
                case 'datetime':
375
                    $swaggerType = 'string';
376
                    $swaggerFormat = 'date-time	';
377
                    break;
378
379
            }
380
            return [$swaggerType, $swaggerFormat];
381
        }
382
383
        /**
384
         * Method that parse the definitions for the api's
385
         * @param array $endpoint
386
         *
387
         * @return array
388
         */
389
        public static function extractSwaggerDefinition(array $endpoint)
390
        {
391
            $definitions = [];
392
            if (array_key_exists('definitions', $endpoint)) {
393
                foreach ($endpoint['definitions'] as $dtoName => $definition) {
394
                    $dto = [
395
                        "type" => "object",
396
                        "properties" => [],
397
                    ];
398
                    foreach ($definition as $field => $format) {
399
                        if (is_array($format)) {
400
                            $subDtoName = preg_replace('/Dto$/', '', $dtoName);
401
                            $subDtoName = preg_replace('/DtoList$/', '', $subDtoName);
402
                            $subDto = self::extractSwaggerDefinition(['definitions' => [
403
                                $subDtoName => $format,
404
                            ]]);
405
                            if (array_key_exists($subDtoName, $subDto)) {
406
                                $definitions = $subDto;
407
                            } else {
408
                                $definitions[$subDtoName] = $subDto;
409
                            }
410
                            $dto['properties'][$field] = [
411
                                '$ref' => "#/definitions/" . $subDtoName,
412
                            ];
413
                        } else {
414
                            list($type, $format) = self::translateSwaggerFormats($format);
415
                            $dto['properties'][$field] = [
416
                                "type" => $type,
417
                            ];
418
                            if (strlen($format)) {
419
                               $dto['properties'][$field]['format'] = $format;
420
                            }
421
                        }
422
                    }
423
                    $definitions[$dtoName] = $dto;
424
                }
425
            }
426
            return $definitions;
427
        }
428
429
        /**
430
         * Method that export
431
         * @param array $modules
432
         *
433
         * @return array
434
         */
435
        public static function swaggerFormatter(array $modules = [])
436
        {
437
            $endpoints = [];
438
            pre($modules, true);
439
            $dtos = [];
440
            $formatted = [
441
                "swagger" => "2.0",
442
                "host" => Router::getInstance()->getRoute(''),
443
                "basePath" => "/api",
444
                "schemes" => ["http", "https"],
445
                "externalDocs" => [
446
                    "description" => "Principal Url",
447
                    "url" => Router::getInstance()->getRoute(''),
448
                ]
449
            ];
450
            foreach($endpoints as $model) {
451
                foreach ($model as $endpoint) {
452
                    $dtos += self::extractSwaggerDefinition($endpoint);
453
                }
454
            }
455
            $formatted['definitions'] = $dtos;
456
            return $formatted;
457
        }
458
459
        /**
460
         * Method that extract the Dto class for the api documentation
461
         * @param string $dto
462
         * @param boolean $isArray
463
         *
464
         * @return string
465
         */
466
        protected function extractDtoName($dto, $isArray = false)
467
        {
468
            $dto = explode('\\', $dto);
469
            $modelDto = array_pop($dto) . "Dto";
470
            if ($isArray) {
471
                $modelDto .= "List";
472
            }
473
474
            return $modelDto;
475
        }
476
    }
477