Completed
Push — master ( daf0f3...03b505 )
by Fran
09:59
created

DocumentorService::extractRoute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 7
rs 9.4285
1
<?php
2
    namespace PSFS\services;
3
4
    use PSFS\base\Logger;
5
    use PSFS\base\Service;
6
    use Symfony\Component\Finder\Finder;
7
8
    /**
9
     * Class DocumentorService
10
     * @package PSFS\services
11
     */
12
    class DocumentorService extends Service
13
    {
14
        const DTO_INTERFACE = '\\PSFS\\base\\dto\\Dto';
15
        const MODEL_INTERFACE = '\\Propel\\Runtime\\ActiveRecord\\ActiveRecordInterface';
16
        /**
17
         * @Inyectable
18
         * @var \PSFS\base\Router route
19
         */
20
        protected $route;
21
22
        /**
23
         * Method that extract all modules
24
         * @return array
25
         */
26
        public function getModules()
27
        {
28
            $modules = [];
29
            $domains = $this->route->getDomains();
30
            if (count($domains)) {
31
                foreach (array_keys($domains) as $domain) {
32
                    try {
33
                        if (!preg_match('/^\@ROOT/i', $domain)) {
34
                            $modules[] = str_replace('/', '', str_replace('@', '', $domain));
35
                        }
36
                    } catch (\Exception $e) {
37
                        $modules[] = $e->getMessage();
38
                    }
39
                }
40
            }
41
42
            return $modules;
43
        }
44
45
        /**
46
         * Method that extract all endpoints for each module
47
         *
48
         * @param string $module
49
         *
50
         * @return array
51
         */
52
        public function extractApiEndpoints($module)
53
        {
54
            $module_path = CORE_DIR . DIRECTORY_SEPARATOR . $module . DIRECTORY_SEPARATOR . "Api";
55
            $endpoints = [];
56
            if (file_exists($module_path)) {
57
                $finder = new Finder();
58
                $finder->files()->depth('== 0')->in($module_path)->name('*.php');
59
                if (count($finder)) {
60
                    /** @var \SplFileInfo $file */
61
                    foreach ($finder as $file) {
62
                        $namespace = "\\{$module}\\Api\\" . str_replace('.php', '', $file->getFilename());
63
                        $endpoints[$namespace] = $this->extractApiInfo($namespace);
64
                    }
65
                }
66
            }
67
68
            return $endpoints;
69
        }
70
71
        /**
72
         * Method that extract all the endpoit information by reflection
73
         *
74
         * @param string $namespace
75
         *
76
         * @return array
77
         */
78
        public function extractApiInfo($namespace)
79
        {
80
            $info = [];
81
            $reflection = new \ReflectionClass($namespace);
82
            $publicMethods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
83
            if (count($publicMethods)) {
84
                /** @var \ReflectionMethod $method */
85
                foreach ($publicMethods as $method) {
86
                    try {
87
                        $mInfo = $this->extractMethodInfo($namespace, $method, $reflection);
88
                        if (NULL !== $mInfo) {
89
                            $info[] = $mInfo;
90
                        }
91
                    } catch (\Exception $e) {
92
                        Logger::getInstance()->errorLog($e->getMessage());
93
                    }
94
                }
95
            }
96
97
            return $info;
98
        }
99
100
        /**
101
         * Extract route from doc comments
102
         *
103
         * @param string $comments
104
         *
105
         * @return string
106
         */
107
        protected function extractRoute($comments = '')
108
        {
109
            $route = '';
110
            preg_match('/@route\ (.*)\n/i', $comments, $route);
111
112
            return $route[1];
113
        }
114
115
        /**
116
         * Extract method from doc comments
117
         *
118
         * @param string $comments
119
         *
120
         * @return string
121
         */
122
        protected function extractMethod($comments = '')
123
        {
124
            $method = 'GET';
125
            preg_match('/@(get|post|put|delete)\n/i', $comments, $method);
126
127
            return strtoupper($method[1]);
128
        }
129
130
        /**
131
         * Extract visibility from doc comments
132
         *
133
         * @param string $comments
134
         *
135
         * @return boolean
136
         */
137
        protected function extractVisibility($comments = '')
138
        {
139
            $visible = TRUE;
140
            preg_match('/@visible\ (true|false)\n/i', $comments, $visibility);
141
            if (count($visibility)) {
142
                $visible = !('false' == $visibility[1]);
143
            }
144
145
            return $visible;
146
        }
147
148
        /**
149
         * Method that extract the description for the endpoint
150
         *
151
         * @param string $comments
152
         *
153
         * @return string
154
         */
155
        protected function extractDescription($comments = '')
156
        {
157
            $description = '';
158
            $docs = explode("\n", $comments);
159
            if (count($docs)) {
160
                foreach ($docs as &$doc) {
161 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...
162
                        $doc = explode('* ', $doc);
163
                        $description = $doc[1];
164
                    }
165
                }
166
            }
167
168
            return $description;
169
        }
170
171
        /**
172
         * Method that extract the type of a variable
173
         *
174
         * @param string $comments
175
         *
176
         * @return string
177
         */
178
        protected function extractVarType($comments = '')
179
        {
180
            $type = 'string';
181
            preg_match('/@var\ (.*) (.*)\n/i', $comments, $varType);
182
            if (count($varType)) {
183
                $aux = trim($varType[1]);
184
                $type = str_replace(' ', '', strlen($aux) > 0 ? $varType[1] : $varType[2]);
185
            }
186
187
            return $type;
188
        }
189
190
        /**
191
         * Method that extract the payload for the endpoint
192
         *
193
         * @param string $model
194
         * @param string $comments
195
         *
196
         * @return array
197
         */
198
        protected function extractPayload($model, $comments = '')
199
        {
200
            $payload = [];
201
            preg_match('/@payload\ (.*)\n/i', $comments, $doc);
202
            if (count($doc)) {
203
                $namespace = str_replace('{__API__}', $model, $doc[1]);
204
                $payload = $this->extractModelFields($namespace);
205
            }
206
207
            return $payload;
208
        }
209
210
        /**
211
         * Extract all the properties from Dto class
212
         *
213
         * @param string $class
214
         *
215
         * @return array
216
         */
217
        protected function extractDtoProperties($class)
218
        {
219
            $properties = [];
220
            $reflector = new \ReflectionClass($class);
221
            if ($reflector->isSubclassOf(self::DTO_INTERFACE)) {
222 View Code Duplication
                foreach ($reflector->getProperties(\ReflectionMethod::IS_PUBLIC) as $property) {
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...
223
                    $type = $this->extractVarType($property->getDocComment());
224
                    if(class_exists($type)) {
225
                        $properties[$property->getName()] = $this->extractModelFields($type);
226
                    } else {
227
                        $properties[$property->getName()] = $type;
228
                    }
229
                }
230
            }
231
232
            return $properties;
233
        }
234
235
        /**
236
         * Extract return class for api endpoint
237
         *
238
         * @param string $model
239
         * @param string $comments
240
         *
241
         * @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...
242
         */
243
        protected function extractReturn($model, $comments = '')
244
        {
245
            $modelDto  = [];
246
            preg_match('/\@return\ (.*)\((.*)\)\n/i', $comments, $returnTypes);
247
            if (count($returnTypes)) {
248
                // Extract principal DTO information
249
                if (array_key_exists(1, $returnTypes)) {
250
                    $modelDto = $this->extractDtoProperties($returnTypes[1]);
251
                }
252
                if (array_key_exists(2, $returnTypes)) {
253
                    $subDtos = preg_split('/,?\ /', str_replace('{__API__}', $model, $returnTypes[2]));
254
                    if (count($subDtos)) {
255
                        foreach ($subDtos as $subDto) {
256
                            $isArray = false;
257
                            list($field, $dto) = explode('=', $subDto);
258
                            if (false !== strpos($dto, '[') && false !== strpos($dto, ']')) {
259
                                $dto = str_replace(']', '', str_replace('[', '', $dto));
260
                                $isArray = true;
261
                            }
262
                            $dto = $this->extractModelFields($dto);
263
                            $modelDto[$field] = ($isArray) ? [$dto] : $dto;
264
                        }
265
                    }
266
                }
267
            }
268
269
            return $modelDto;
270
        }
271
272
        /**
273
         * Extract all fields from a ActiveResource model
274
         *
275
         * @param string $namespace
276
         *
277
         * @return mixed
278
         */
279
        protected function extractModelFields($namespace)
280
        {
281
            $payload = [];
282
            try {
283
                $reflector = new \ReflectionClass($namespace);
284
                // Checks if reflector is a subclass of propel ActiveRecords
285
                if (NULL !== $reflector && $reflector->isSubclassOf(self::MODEL_INTERFACE)) {
286
                    $tableMap = $namespace::TABLE_MAP;
287
                    $fieldNames = $tableMap::getFieldNames();
288
                    if (count($fieldNames)) {
289
                        foreach ($fieldNames as $field) {
290
                            $variable = $reflector->getProperty(strtolower($field));
291
                            $varDoc = $variable->getDocComment();
292
                            $payload[$field] = $this->extractVarType($varDoc);
293
                        }
294
                    }
295
                } elseif (null !== $reflector && $reflector->isSubclassOf(self::DTO_INTERFACE)) {
296
                    $payload = $this->extractDtoProperties($namespace);
297
                }
298
            } catch (\Exception $e) {
299
                Logger::getInstance()->errorLog($e->getMessage());
300
            }
301
302
            return $payload;
303
        }
304
305
        /**
306
         * Method that extract all the needed info for each method in each API
307
         *
308
         * @param string $namespace
309
         * @param \ReflectionMethod $method
310
         * @param \ReflectionClass $reflection
311
         *
312
         * @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...
313
         */
314
        protected function extractMethodInfo($namespace, \ReflectionMethod $method, \ReflectionClass $reflection)
315
        {
316
            $methodInfo = NULL;
317
            $docComments = $method->getDocComment();
318
            $shortName = $reflection->getShortName();
319
            $modelNamespace = str_replace('Api', 'Models', $namespace);
320
            if (FALSE !== $docComments && preg_match('/\@route\ /i', $docComments)) {
321
                $visibility = $this->extractVisibility($docComments);
322
                $route = str_replace('{__API__}', $shortName, $this->extractRoute($docComments));
323
                if ($visibility && preg_match('/^\/api\//i', $route)) {
324
                    try {
325
                        $methodInfo = [
326
                            'url'         => $route,
327
                            'method'      => $this->extractMethod($docComments),
328
                            '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...
329
                            'return'      => $this->extractReturn($modelNamespace, $docComments),
330
                        ];
331
                        if (in_array($methodInfo['method'], ['POST', 'PUT'])) {
332
                            $methodInfo['payload'] = $this->extractPayload($modelNamespace, $docComments);
333
                        }
334
                    } catch (\Exception $e) {
335
                        jpre($e->getMessage());
336
                        Logger::getInstance()->errorLog($e->getMessage());
337
                    }
338
                }
339
            }
340
341
            return $methodInfo;
342
        }
343
344
        /**
345
         * Translator from php types to swagger types
346
         * @param string $format
347
         *
348
         * @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...
349
         */
350
        public static function translateSwaggerFormats($format)
351
        {
352
            switch(strtolower($format)) {
353
                case 'bool':
354
                case 'boolean':
355
                    $swaggerType = 'boolean';
356
                    $swaggerFormat = '';
357
                    break;
358
                default:
359
                case 'string':
1 ignored issue
show
Unused Code introduced by
case 'string': does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
360
                case 'varchar':
361
                    $swaggerType = 'string';
362
                    $swaggerFormat = '';
363
                    break;
364
                case 'binary':
365
                case 'varbinary':
366
                    $swaggerType = 'string';
367
                    $swaggerFormat = 'binary';
368
                    break;
369
                case 'int':
370
                case 'integer':
371
                case 'float':
372
                case 'double':
373
                    $swaggerType = 'integer';
374
                    $swaggerFormat = 'int32';
375
                    break;
376
                case 'date':
377
                    $swaggerType = 'string';
378
                    $swaggerFormat = 'date';
379
                    break;
380
                case 'datetime':
381
                    $swaggerType = 'string';
382
                    $swaggerFormat = 'date-time	';
383
                    break;
384
385
            }
386
            return [$swaggerType, $swaggerFormat];
387
        }
388
389
        /**
390
         * Method that parse the definitions for the api's
391
         * @param array $endpoint
392
         *
393
         * @return array
394
         */
395
        public static function extractSwaggerDefinition(array $endpoint)
396
        {
397
            $definitions = [];
398
            if (array_key_exists('definitions', $endpoint)) {
399
                foreach ($endpoint['definitions'] as $dtoName => $definition) {
400
                    $dto = [
401
                        "type" => "object",
402
                        "properties" => [],
403
                    ];
404
                    foreach ($definition as $field => $format) {
405
                        if (is_array($format)) {
406
                            $subDtoName = preg_replace('/Dto$/', '', $dtoName);
407
                            $subDtoName = preg_replace('/DtoList$/', '', $subDtoName);
408
                            $subDto = self::extractSwaggerDefinition(['definitions' => [
409
                                $subDtoName => $format,
410
                            ]]);
411
                            if (array_key_exists($subDtoName, $subDto)) {
412
                                $definitions = $subDto;
413
                            } else {
414
                                $definitions[$subDtoName] = $subDto;
415
                            }
416
                            $dto['properties'][$field] = [
417
                                '$ref' => "#/definitions/" . $subDtoName,
418
                            ];
419
                        } else {
420
                            list($type, $format) = self::translateSwaggerFormats($format);
421
                            $dto['properties'][$field] = [
422
                                "type" => $type,
423
                            ];
424
                            if (strlen($format)) {
425
                               $dto['properties'][$field]['format'] = $format;
426
                            }
427
                        }
428
                    }
429
                    $definitions[$dtoName] = $dto;
430
                }
431
            }
432
            return $definitions;
433
        }
434
435
        /**
436
         * Method that export
437
         * @param array $modules
438
         *
439
         * @return array
440
         */
441
        public static function swaggerFormatter(array $modules = [])
442
        {
443
            $endpoints = [];
444
            pre($modules, true);
445
            $dtos = [];
446
            $formatted = [
447
                "swagger" => "2.0",
448
                "host" => Router::getInstance()->getRoute(''),
449
                "basePath" => "/api",
450
                "schemes" => ["http", "https"],
451
                "externalDocs" => [
452
                    "description" => "Principal Url",
453
                    "url" => Router::getInstance()->getRoute(''),
454
                ]
455
            ];
456
            foreach($endpoints as $model) {
457
                foreach ($model as $endpoint) {
458
                    $dtos += self::extractSwaggerDefinition($endpoint);
459
                }
460
            }
461
            $formatted['definitions'] = $dtos;
462
            return $formatted;
463
        }
464
465
        /**
466
         * Method that extract the Dto class for the api documentation
467
         * @param string $dto
468
         * @param boolean $isArray
469
         *
470
         * @return string
471
         */
472
        protected function extractDtoName($dto, $isArray = false)
473
        {
474
            $dto = explode('\\', $dto);
475
            $modelDto = array_pop($dto) . "Dto";
476
            if ($isArray) {
477
                $modelDto .= "List";
478
            }
479
480
            return $modelDto;
481
        }
482
    }
483