Passed
Push — master ( 0a9e87...9dd3d7 )
by Fran
03:31
created

DocumentorService::extractApiInfo()   B

Complexity

Conditions 5
Paths 2

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 2
nop 2
dl 0
loc 18
rs 8.8571
c 0
b 0
f 0
1
<?php
2
    namespace PSFS\services;
3
4
    use Propel\Runtime\Map\TableMap;
5
    use PSFS\base\Logger;
6
    use PSFS\base\Service;
7
    use PSFS\base\types\helpers\InjectorHelper;
8
    use PSFS\base\types\helpers\RouterHelper;
9
    use Symfony\Component\Finder\Finder;
10
11
    /**
12
     * Class DocumentorService
13
     * @package PSFS\services
14
     */
15
    class DocumentorService extends Service
16
    {
17
        const DTO_INTERFACE = '\\PSFS\\base\\dto\\Dto';
18
        const MODEL_INTERFACE = '\\Propel\\Runtime\\ActiveRecord\\ActiveRecordInterface';
19
        /**
20
         * @Inyectable
21
         * @var \PSFS\base\Router route
22
         */
23
        protected $route;
24
25
        /**
26
         * Method that extract all modules
27
         * @return array
28
         */
29
        public function getModules()
30
        {
31
            $modules = [];
32
            $domains = $this->route->getDomains();
33
            if (count($domains)) {
34
                foreach ($domains as $module => $info) {
35
                    try {
36
                        $module = str_replace('/', '', str_replace('@', '', $module));
37
                        if (!preg_match('/^ROOT/i', $module)) {
38
                            $modules[] = [
39
                                'name' => $module,
40
                                'path' => realpath($info['template'] . DIRECTORY_SEPARATOR . '..'),
41
                            ];
42
                        }
43
                    } catch (\Exception $e) {
44
                        $modules[] = $e->getMessage();
45
                    }
46
                }
47
            }
48
49
            return $modules;
50
        }
51
52
        /**
53
         * Method that extract all endpoints for each module
54
         *
55
         * @param array $module
56
         *
57
         * @return array
58
         */
59
        public function extractApiEndpoints(array $module)
60
        {
61
            $module_path = $module['path'] . DIRECTORY_SEPARATOR . 'Api';
62
            $module_name = $module['name'];
63
            $endpoints = [];
64
            if (file_exists($module_path)) {
65
                $finder = new Finder();
66
                $finder->files()->in($module_path)->depth(0)->name('*.php');
67
                if (count($finder)) {
68
                    /** @var \SplFileInfo $file */
69
                    foreach ($finder as $file) {
70
                        $namespace = "\\{$module_name}\\Api\\" . str_replace('.php', '', $file->getFilename());
71
                        $info = $this->extractApiInfo($namespace, $module_name);
72
                        if(!empty($info)) {
73
                            $endpoints[$namespace] = $info;
74
                        }
75
                    }
76
                }
77
            }
78
            return $endpoints;
79
        }
80
81
        /**
82
         * Method that extract all the endpoit information by reflection
83
         *
84
         * @param string $namespace
85
         *
86
         * @return array
87
         */
88
        public function extractApiInfo($namespace, $module)
89
        {
90
            $info = [];
91
            if(class_exists($namespace)) {
92
                $reflection = new \ReflectionClass($namespace);
93
                foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
94
                    try {
95
                        $mInfo = $this->extractMethodInfo($namespace, $method, $reflection, $module);
96
                        if (NULL !== $mInfo) {
97
                            $info[] = $mInfo;
98
                        }
99
                    } catch (\Exception $e) {
100
                        Logger::getInstance()->errorLog($e->getMessage());
101
                    }
102
                }
103
            }
104
            return $info;
105
        }
106
107
        /**
108
         * Extract route from doc comments
109
         *
110
         * @param string $comments
111
         *
112
         * @return string
113
         */
114
        protected function extractRoute($comments = '')
115
        {
116
            $route = '';
117
            preg_match('/@route\ (.*)\n/i', $comments, $route);
118
119
            return $route[1];
120
        }
121
122
        /**
123
         * Extract api from doc comments
124
         *
125
         * @param string $comments
126
         *
127
         * @return string
128
         */
129
        protected function extractApi($comments = '')
130
        {
131
            $api = '';
132
            preg_match('/@api\ (.*)\n/i', $comments, $api);
133
134
            return $api[1];
135
        }
136
137
        /**
138
         * Extract api from doc comments
139
         *
140
         * @param string $comments
141
         *
142
         * @return boolean
143
         */
144
        protected function checkDeprecated($comments = '')
145
        {
146
            return false != preg_match('/@deprecated\n/i', $comments);
1 ignored issue
show
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/@deprecated\\n/i', $comments) of type integer to the boolean false. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
147
        }
148
149
        /**
150
         * Extract visibility from doc comments
151
         *
152
         * @param string $comments
153
         *
154
         * @return boolean
155
         */
156
        protected function extractVisibility($comments = '')
157
        {
158
            $visible = TRUE;
159
            preg_match('/@visible\ (true|false)\n/i', $comments, $visibility);
160
            if (count($visibility)) {
161
                $visible = !('false' == $visibility[1]);
162
            }
163
164
            return $visible;
165
        }
166
167
        /**
168
         * Method that extract the description for the endpoint
169
         *
170
         * @param string $comments
171
         *
172
         * @return string
173
         */
174
        protected function extractDescription($comments = '')
175
        {
176
            $description = '';
177
            $docs = explode("\n", $comments);
178
            if (count($docs)) {
179
                foreach ($docs as &$doc) {
180 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...
181
                        $doc = explode('* ', $doc);
182
                        $description = $doc[1];
183
                    }
184
                }
185
            }
186
187
            return $description;
188
        }
189
190
        /**
191
         * Method that extract the type of a variable
192
         *
193
         * @param string $comments
194
         *
195
         * @return string
196
         */
197
        public static function extractVarType($comments = '')
198
        {
199
            $type = 'string';
200
            preg_match('/@var\ (.*) (.*)\n/i', $comments, $varType);
201
            if (count($varType)) {
202
                $aux = trim($varType[1]);
203
                $type = str_replace(' ', '', strlen($aux) > 0 ? $varType[1] : $varType[2]);
204
            }
205
206
            return $type;
207
        }
208
209
        /**
210
         * Method that extract the payload for the endpoint
211
         *
212
         * @param string $model
213
         * @param string $comments
214
         *
215
         * @return array
216
         */
217
        protected function extractPayload($model, $comments = '')
218
        {
219
            $payload = [];
220
            preg_match('/@payload\ (.*)\n/i', $comments, $doc);
221
            if (count($doc)) {
222
                $namespace = str_replace('{__API__}', $model, $doc[1]);
223
                $payload = $this->extractModelFields($namespace);
224
            }
225
226
            return $payload;
227
        }
228
229
        /**
230
         * Extract all the properties from Dto class
231
         *
232
         * @param string $class
233
         *
234
         * @return array
235
         */
236
        protected function extractDtoProperties($class)
237
        {
238
            $properties = [];
239
            $reflector = new \ReflectionClass($class);
240
            if ($reflector->isSubclassOf(self::DTO_INTERFACE)) {
241
	            $properties = array_merge($properties, InjectorHelper::extractProperties($reflector, \ReflectionMethod::IS_PUBLIC));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 129 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...
242
            }
243
244
            return $properties;
245
        }
246
247
        /**
248
         * Extract return class for api endpoint
249
         *
250
         * @param string $model
251
         * @param string $comments
252
         *
253
         * @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...
254
         */
255
        protected function extractReturn($model, $comments = '')
256
        {
257
            $modelDto  = [];
258
            preg_match('/\@return\ (.*)\((.*)\)\n/i', $comments, $returnTypes);
259
            if (count($returnTypes)) {
260
                // Extract principal DTO information
261
                if (array_key_exists(1, $returnTypes)) {
262
                    $modelDto = $this->extractDtoProperties($returnTypes[1]);
263
                }
264
                if (array_key_exists(2, $returnTypes)) {
265
                    $subDtos = preg_split('/,?\ /', str_replace('{__API__}', $model, $returnTypes[2]));
266
                    if (count($subDtos)) {
267
                        foreach ($subDtos as $subDto) {
268
                            $isArray = false;
269
                            list($field, $dto) = explode('=', $subDto);
270
                            if (false !== strpos($dto, '[') && false !== strpos($dto, ']')) {
271
                                $dto = str_replace(']', '', str_replace('[', '', $dto));
272
                                $isArray = true;
273
                            }
274
                            $dto = $this->extractModelFields($dto);
275
                            $modelDto[$field] = ($isArray) ? [$dto] : $dto;
276
                        }
277
                    }
278
                }
279
            }
280
281
            return $modelDto;
282
        }
283
284
        /**
285
         * Extract all fields from a ActiveResource model
286
         *
287
         * @param string $namespace
288
         *
289
         * @return mixed
290
         */
291
        protected function extractModelFields($namespace)
292
        {
293
            $payload = [];
294
            try {
295
                $reflector = new \ReflectionClass($namespace);
296
                // Checks if reflector is a subclass of propel ActiveRecords
297
                if (NULL !== $reflector && $reflector->isSubclassOf(self::MODEL_INTERFACE)) {
298
                    $tableMap = $namespace::TABLE_MAP;
299
                    $fieldNames = $tableMap::getFieldNames(TableMap::TYPE_FIELDNAME);
300
                    if (count($fieldNames)) {
301
                        foreach ($fieldNames as $field) {
302
                            $variable = $reflector->getProperty(strtolower($field));
303
                            $varDoc = $variable->getDocComment();
304
                            $payload[$tableMap::translateFieldName($field, TableMap::TYPE_FIELDNAME, TableMap::TYPE_PHPNAME)] = $this->extractVarType($varDoc);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 159 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...
305
                        }
306
                    }
307
                } elseif (null !== $reflector && $reflector->isSubclassOf(self::DTO_INTERFACE)) {
308
                    $payload = $this->extractDtoProperties($namespace);
309
                }
310
            } catch (\Exception $e) {
311
                Logger::getInstance()->errorLog($e->getMessage());
312
            }
313
314
            return $payload;
315
        }
316
317
        /**
318
         * Method that extract all the needed info for each method in each API
319
         *
320
         * @param string $namespace
321
         * @param \ReflectionMethod $method
322
         * @param \ReflectionClass $reflection
323
         * @param string $module
324
         *
325
         * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be 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...
326
         */
327
        protected function extractMethodInfo($namespace, \ReflectionMethod $method, \ReflectionClass $reflection, $module)
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 122 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...
328
        {
329
            $methodInfo = NULL;
330
            $docComments = $method->getDocComment();
331
            if (FALSE !== $docComments && preg_match('/\@route\ /i', $docComments)) {
332
                $api = self::extractApi($reflection->getDocComment());
333
                list($route, $info) = RouterHelper::extractRouteInfo($method, $api, $module);
334
                $route = explode('#|#', $route);
335
                $modelNamespace = str_replace('Api', 'Models', $namespace);
336
                if ($info['visible'] && !self::checkDeprecated($docComments)) {
337
                    try {
338
                        $methodInfo = [
339
                            'url'         => array_pop($route),
340
                            'method'      => $info['http'],
341
                            'description' => $info['label'],
342
                            'return'      => $this->extractReturn($modelNamespace, $docComments),
343
                        ];
344
                        if (in_array($methodInfo['method'], ['POST', 'PUT'])) {
345
                            $methodInfo['payload'] = $this->extractPayload($modelNamespace, $docComments);
346
                        }
347
                    } catch (\Exception $e) {
348
                        jpre($e->getMessage());
349
                        Logger::getInstance()->errorLog($e->getMessage());
350
                    }
351
                }
352
            }
353
354
            return $methodInfo;
355
        }
356
357
        /**
358
         * Translator from php types to swagger types
359
         * @param string $format
360
         *
361
         * @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...
362
         */
363
        public static function translateSwaggerFormats($format)
364
        {
365
            switch(strtolower($format)) {
366
                case 'bool':
367
                case 'boolean':
368
                    $swaggerType = 'boolean';
369
                    $swaggerFormat = '';
370
                    break;
371
                default:
372
                case 'string':
373
                case 'varchar':
374
                    $swaggerType = 'string';
375
                    $swaggerFormat = '';
376
                    break;
377
                case 'binary':
378
                case 'varbinary':
379
                    $swaggerType = 'string';
380
                    $swaggerFormat = 'binary';
381
                    break;
382
                case 'int':
383
                case 'integer':
384
                case 'float':
385
                case 'double':
386
                    $swaggerType = 'integer';
387
                    $swaggerFormat = 'int32';
388
                    break;
389
                case 'date':
390
                    $swaggerType = 'string';
391
                    $swaggerFormat = 'date';
392
                    break;
393
                case 'datetime':
394
                    $swaggerType = 'string';
395
                    $swaggerFormat = 'date-time	';
396
                    break;
397
398
            }
399
            return [$swaggerType, $swaggerFormat];
400
        }
401
402
        /**
403
         * Method that parse the definitions for the api's
404
         * @param array $endpoint
405
         *
406
         * @return array
407
         */
408
        public static function extractSwaggerDefinition(array $endpoint)
409
        {
410
            $definitions = [];
411
            if (array_key_exists('definitions', $endpoint)) {
412
                foreach ($endpoint['definitions'] as $dtoName => $definition) {
413
                    $dto = [
414
                        "type" => "object",
415
                        "properties" => [],
416
                    ];
417
                    foreach ($definition as $field => $format) {
418
                        if (is_array($format)) {
419
                            $subDtoName = preg_replace('/Dto$/', '', $dtoName);
420
                            $subDtoName = preg_replace('/DtoList$/', '', $subDtoName);
421
                            $subDto = self::extractSwaggerDefinition(['definitions' => [
422
                                $subDtoName => $format,
423
                            ]]);
424
                            if (array_key_exists($subDtoName, $subDto)) {
425
                                $definitions = $subDto;
426
                            } else {
427
                                $definitions[$subDtoName] = $subDto;
428
                            }
429
                            $dto['properties'][$field] = [
430
                                '$ref' => "#/definitions/" . $subDtoName,
431
                            ];
432
                        } else {
433
                            list($type, $format) = self::translateSwaggerFormats($format);
434
                            $dto['properties'][$field] = [
435
                                "type" => $type,
436
                            ];
437
                            if (strlen($format)) {
438
                               $dto['properties'][$field]['format'] = $format;
439
                            }
440
                        }
441
                    }
442
                    $definitions[$dtoName] = $dto;
443
                }
444
            }
445
            return $definitions;
446
        }
447
448
        /**
449
         * Method that export
450
         * @param array $modules
451
         *
452
         * @return array
453
         */
454
        public static function swaggerFormatter(array $modules = [])
455
        {
456
            $endpoints = [];
457
            pre($modules, true);
458
            $dtos = [];
459
            $formatted = [
460
                "swagger" => "2.0",
461
                "host" => Router::getInstance()->getRoute(''),
462
                "basePath" => "/api",
463
                "schemes" => ["http", "https"],
464
                "externalDocs" => [
465
                    "description" => "Principal Url",
466
                    "url" => Router::getInstance()->getRoute(''),
467
                ]
468
            ];
469
            foreach($endpoints as $model) {
470
                foreach ($model as $endpoint) {
471
                    $dtos += self::extractSwaggerDefinition($endpoint);
472
                }
473
            }
474
            $formatted['definitions'] = $dtos;
475
            return $formatted;
476
        }
477
478
        /**
479
         * Method that extract the Dto class for the api documentation
480
         * @param string $dto
481
         * @param boolean $isArray
482
         *
483
         * @return string
484
         */
485
        protected function extractDtoName($dto, $isArray = false)
486
        {
487
            $dto = explode('\\', $dto);
488
            $modelDto = array_pop($dto) . "Dto";
489
            if ($isArray) {
490
                $modelDto .= "List";
491
            }
492
493
            return $modelDto;
494
        }
495
    }
496