Passed
Push — master ( c957ae...7ee1b6 )
by Fran
03:42
created

DocumentorService::extractApiEndpoints()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 3
nop 1
dl 0
loc 21
rs 8.7624
c 0
b 0
f 0
1
<?php
2
    namespace PSFS\services;
3
4
    use Propel\Runtime\Map\TableMap;
5
    use PSFS\base\config\Config;
6
    use PSFS\base\Logger;
7
    use PSFS\base\Router;
8
    use PSFS\base\Service;
9
    use PSFS\base\types\helpers\InjectorHelper;
10
    use PSFS\base\types\helpers\RouterHelper;
11
    use Symfony\Component\Finder\Finder;
12
13
    /**
14
     * Class DocumentorService
15
     * @package PSFS\services
16
     */
17
    class DocumentorService extends Service
18
    {
19
        const DTO_INTERFACE = '\\PSFS\\base\\dto\\Dto';
20
        const MODEL_INTERFACE = '\\Propel\\Runtime\\ActiveRecord\\ActiveRecordInterface';
21
        /**
22
         * @Inyectable
23
         * @var \PSFS\base\Router route
24
         */
25
        protected $route;
26
27
        /**
28
         * Method that extract all modules
29
         * @return array
30
         */
31
        public function getModules()
32
        {
33
            $modules = [];
34
            $domains = $this->route->getDomains();
35
            if (count($domains)) {
36
                foreach ($domains as $module => $info) {
37
                    try {
38
                        $module = str_replace('/', '', str_replace('@', '', $module));
39
                        if (!preg_match('/^ROOT/i', $module)) {
40
                            $modules[] = [
41
                                'name' => $module,
42
                                'path' => realpath($info['template'] . DIRECTORY_SEPARATOR . '..'),
43
                            ];
44
                        }
45
                    } catch (\Exception $e) {
46
                        $modules[] = $e->getMessage();
47
                    }
48
                }
49
            }
50
51
            return $modules;
52
        }
53
54
        /**
55
         * Method that extract all endpoints for each module
56
         *
57
         * @param array $module
58
         *
59
         * @return array
60
         */
61
        public function extractApiEndpoints(array $module)
62
        {
63
            $module_path = $module['path'] . DIRECTORY_SEPARATOR . 'Api';
64
            $module_name = $module['name'];
65
            $endpoints = [];
66
            if (file_exists($module_path)) {
67
                $finder = new Finder();
68
                $finder->files()->in($module_path)->depth(0)->name('*.php');
69
                if (count($finder)) {
70
                    /** @var \SplFileInfo $file */
71
                    foreach ($finder as $file) {
72
                        $namespace = "\\{$module_name}\\Api\\" . str_replace('.php', '', $file->getFilename());
73
                        $info = $this->extractApiInfo($namespace, $module_name);
74
                        if(!empty($info)) {
75
                            $endpoints[$namespace] = $info;
76
                        }
77
                    }
78
                }
79
            }
80
            return $endpoints;
81
        }
82
83
        /**
84
         * Method that extract all the endpoit information by reflection
85
         *
86
         * @param string $namespace
87
         *
88
         * @return array
89
         */
90
        public function extractApiInfo($namespace, $module)
91
        {
92
            $info = [];
93
            if(class_exists($namespace)) {
94
                $reflection = new \ReflectionClass($namespace);
95
                foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
96
                    try {
97
                        $mInfo = $this->extractMethodInfo($namespace, $method, $reflection, $module);
98
                        if (NULL !== $mInfo) {
99
                            $info[] = $mInfo;
100
                        }
101
                    } catch (\Exception $e) {
102
                        Logger::getInstance()->errorLog($e->getMessage());
103
                    }
104
                }
105
            }
106
            return $info;
107
        }
108
109
        /**
110
         * Extract route from doc comments
111
         *
112
         * @param string $comments
113
         *
114
         * @return string
115
         */
116
        protected function extractRoute($comments = '')
117
        {
118
            $route = '';
119
            preg_match('/@route\ (.*)\n/i', $comments, $route);
120
121
            return $route[1];
122
        }
123
124
        /**
125
         * Extract api from doc comments
126
         *
127
         * @param string $comments
128
         *
129
         * @return string
130
         */
131
        protected function extractApi($comments = '')
132
        {
133
            $api = '';
134
            preg_match('/@api\ (.*)\n/i', $comments, $api);
135
136
            return $api[1];
137
        }
138
139
        /**
140
         * Extract api from doc comments
141
         *
142
         * @param string $comments
143
         *
144
         * @return boolean
145
         */
146
        protected function checkDeprecated($comments = '')
147
        {
148
            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...
149
        }
150
151
        /**
152
         * Extract visibility from doc comments
153
         *
154
         * @param string $comments
155
         *
156
         * @return boolean
157
         */
158
        protected function extractVisibility($comments = '')
159
        {
160
            $visible = TRUE;
161
            preg_match('/@visible\ (true|false)\n/i', $comments, $visibility);
162
            if (count($visibility)) {
163
                $visible = !('false' == $visibility[1]);
164
            }
165
166
            return $visible;
167
        }
168
169
        /**
170
         * Method that extract the description for the endpoint
171
         *
172
         * @param string $comments
173
         *
174
         * @return string
175
         */
176
        protected function extractDescription($comments = '')
177
        {
178
            $description = '';
179
            $docs = explode("\n", $comments);
180
            if (count($docs)) {
181
                foreach ($docs as &$doc) {
182 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...
183
                        $doc = explode('* ', $doc);
184
                        $description = $doc[1];
185
                    }
186
                }
187
            }
188
189
            return $description;
190
        }
191
192
        /**
193
         * Method that extract the type of a variable
194
         *
195
         * @param string $comments
196
         *
197
         * @return string
198
         */
199
        public static function extractVarType($comments = '')
200
        {
201
            $type = 'string';
202
            preg_match('/@var\ (.*) (.*)\n/i', $comments, $varType);
203
            if (count($varType)) {
204
                $aux = trim($varType[1]);
205
                $type = str_replace(' ', '', strlen($aux) > 0 ? $varType[1] : $varType[2]);
206
            }
207
208
            return $type;
209
        }
210
211
        /**
212
         * Method that extract the payload for the endpoint
213
         *
214
         * @param string $model
215
         * @param string $comments
216
         *
217
         * @return array
218
         */
219
        protected function extractPayload($model, $comments = '')
220
        {
221
            $payload = [];
222
            preg_match('/@payload\ (.*)\n/i', $comments, $doc);
223
            if (count($doc)) {
224
                $namespace = str_replace('{__API__}', $model, $doc[1]);
225
                $payload = $this->extractModelFields($namespace);
226
            }
227
228
            return $payload;
229
        }
230
231
        /**
232
         * Extract all the properties from Dto class
233
         *
234
         * @param string $class
235
         *
236
         * @return array
237
         */
238
        protected function extractDtoProperties($class)
239
        {
240
            $properties = [];
241
            $reflector = new \ReflectionClass($class);
242
            if ($reflector->isSubclassOf(self::DTO_INTERFACE)) {
243
                $properties = array_merge($properties, InjectorHelper::extractVariables($reflector, \ReflectionMethod::IS_PUBLIC));
0 ignored issues
show
Unused Code introduced by
The call to InjectorHelper::extractVariables() has too many arguments starting with \ReflectionMethod::IS_PUBLIC.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 131 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...
244
            }
245
246
            return $properties;
247
        }
248
249
        /**
250
         * Extract return class for api endpoint
251
         *
252
         * @param string $model
253
         * @param string $comments
254
         *
255
         * @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...
256
         */
257
        protected function extractReturn($model, $comments = '')
258
        {
259
            $modelDto  = [];
260
            preg_match('/\@return\ (.*)\((.*)\)\n/i', $comments, $returnTypes);
261
            if (count($returnTypes)) {
262
                // Extract principal DTO information
263
                if (array_key_exists(1, $returnTypes)) {
264
                    $modelDto = $this->extractDtoProperties($returnTypes[1]);
265
                }
266
                if (array_key_exists(2, $returnTypes)) {
267
                    $subDtos = preg_split('/,?\ /', str_replace('{__API__}', $model, $returnTypes[2]));
268
                    if (count($subDtos)) {
269
                        foreach ($subDtos as $subDto) {
270
                            $isArray = false;
271
                            list($field, $dto) = explode('=', $subDto);
272
                            if (false !== strpos($dto, '[') && false !== strpos($dto, ']')) {
273
                                $dto = str_replace(']', '', str_replace('[', '', $dto));
274
                                $isArray = true;
275
                            }
276
                            $dto = $this->extractModelFields($dto);
277
                            $modelDto[$field] = ($isArray) ? [$dto] : $dto;
278
                        }
279
                    }
280
                }
281
            }
282
283
            return $modelDto;
284
        }
285
286
        /**
287
         * Extract all fields from a ActiveResource model
288
         *
289
         * @param string $namespace
290
         *
291
         * @return mixed
292
         */
293
        protected function extractModelFields($namespace)
294
        {
295
            $payload = [];
296
            try {
297
                $reflector = new \ReflectionClass($namespace);
298
                // Checks if reflector is a subclass of propel ActiveRecords
299
                if (NULL !== $reflector && $reflector->isSubclassOf(self::MODEL_INTERFACE)) {
300
                    $tableMap = $namespace::TABLE_MAP;
301
                    $fieldNames = $tableMap::getFieldNames(TableMap::TYPE_FIELDNAME);
302
                    if (count($fieldNames)) {
303
                        foreach ($fieldNames as $field) {
304
                            $variable = $reflector->getProperty(strtolower($field));
305
                            $varDoc = $variable->getDocComment();
306
                            $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...
307
                        }
308
                    }
309
                } elseif (null !== $reflector && $reflector->isSubclassOf(self::DTO_INTERFACE)) {
310
                    $payload = $this->extractDtoProperties($namespace);
311
                }
312
            } catch (\Exception $e) {
313
                Logger::getInstance()->errorLog($e->getMessage());
314
            }
315
316
            return $payload;
317
        }
318
319
        /**
320
         * Method that extract all the needed info for each method in each API
321
         *
322
         * @param string $namespace
323
         * @param \ReflectionMethod $method
324
         * @param \ReflectionClass $reflection
325
         * @param string $module
326
         *
327
         * @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...
328
         */
329
        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...
330
        {
331
            $methodInfo = NULL;
332
            $docComments = $method->getDocComment();
333
            if (FALSE !== $docComments && preg_match('/\@route\ /i', $docComments)) {
334
                $api = self::extractApi($reflection->getDocComment());
335
                list($route, $info) = RouterHelper::extractRouteInfo($method, $api, $module);
336
                $route = explode('#|#', $route);
337
                $modelNamespace = str_replace('Api', 'Models', $namespace);
338
                if ($info['visible'] && !self::checkDeprecated($docComments)) {
339
                    try {
340
                        $methodInfo = [
341
                            'url'         => array_pop($route),
342
                            'method'      => $info['http'],
343
                            'description' => $info['label'],
344
                            'return'      => $this->extractReturn($modelNamespace, $docComments),
345
                        ];
346
                        if (in_array($methodInfo['method'], ['POST', 'PUT'])) {
347
                            $methodInfo['payload'] = $this->extractPayload($modelNamespace, $docComments);
348
                        }
349
                    } catch (\Exception $e) {
350
                        jpre($e->getMessage());
351
                        Logger::getInstance()->errorLog($e->getMessage());
352
                    }
353
                }
354
            }
355
356
            return $methodInfo;
357
        }
358
359
        /**
360
         * Translator from php types to swagger types
361
         * @param string $format
362
         *
363
         * @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...
364
         */
365
        public static function translateSwaggerFormats($format)
366
        {
367
            switch(strtolower($format)) {
368
                case 'bool':
369
                case 'boolean':
370
                    $swaggerType = 'boolean';
371
                    $swaggerFormat = '';
372
                    break;
373
                default:
374
                case 'string':
375
                case 'varchar':
376
                    $swaggerType = 'string';
377
                    $swaggerFormat = '';
378
                    break;
379
                case 'binary':
380
                case 'varbinary':
381
                    $swaggerType = 'string';
382
                    $swaggerFormat = 'binary';
383
                    break;
384
                case 'int':
385
                case 'integer':
386
                case 'float':
387
                case 'double':
388
                    $swaggerType = 'integer';
389
                    $swaggerFormat = 'int32';
390
                    break;
391
                case 'date':
392
                    $swaggerType = 'string';
393
                    $swaggerFormat = 'date';
394
                    break;
395
                case 'datetime':
396
                    $swaggerType = 'string';
397
                    $swaggerFormat = 'date-time	';
398
                    break;
399
400
            }
401
            return [$swaggerType, $swaggerFormat];
402
        }
403
404
        /**
405
         * Method that parse the definitions for the api's
406
         * @param array $endpoint
407
         *
408
         * @return array
409
         */
410
        public static function extractSwaggerDefinition(array $endpoint)
411
        {
412
            $definitions = [];
413
            if (array_key_exists('definitions', $endpoint)) {
414
                foreach ($endpoint['definitions'] as $dtoName => $definition) {
415
                    $dto = [
416
                        "type" => "object",
417
                        "properties" => [],
418
                    ];
419
                    foreach ($definition as $field => $format) {
420
                        if (is_array($format)) {
421
                            $subDtoName = preg_replace('/Dto$/', '', $dtoName);
422
                            $subDtoName = preg_replace('/DtoList$/', '', $subDtoName);
423
                            $subDto = self::extractSwaggerDefinition(['definitions' => [
424
                                $subDtoName => $format,
425
                            ]]);
426
                            if (array_key_exists($subDtoName, $subDto)) {
427
                                $definitions = $subDto;
428
                            } else {
429
                                $definitions[$subDtoName] = $subDto;
430
                            }
431
                            $dto['properties'][$field] = [
432
                                '$ref' => "#/definitions/" . $subDtoName,
433
                            ];
434
                        } else {
435
                            list($type, $format) = self::translateSwaggerFormats($format);
436
                            $dto['properties'][$field] = [
437
                                "type" => $type,
438
                            ];
439
                            if (strlen($format)) {
440
                               $dto['properties'][$field]['format'] = $format;
441
                            }
442
                        }
443
                    }
444
                    $definitions[$dtoName] = $dto;
445
                }
446
            }
447
            return $definitions;
448
        }
449
450
        /**
451
         * Method that export
452
         * @param array $modules
453
         *
454
         * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string|null|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...
455
         */
456
        public static function swaggerFormatter(array $modules = [])
0 ignored issues
show
Unused Code introduced by
The parameter $modules is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
457
        {
458
            $endpoints = [];
459
            $dtos = [];
460
            $formatted = [
461
                "swagger" => "2.0",
462
                "host" => Router::getInstance()->getRoute('', true),
463
                "schemes" => ["http", "https"],
464
                "info" => [
465
                    "title" => Config::getParam('platform_name', 'PSFS'),
466
                    "version" => Config::getParam('api.version', '1.0'),
467
                    "contact" => [
468
                        "name" => Config::getParam("author", "Fran López"),
469
                        "email" => Config::getParam("author_email", "[email protected]"),
470
                    ]
471
                ]
472
            ];
473
            foreach($endpoints as $model) {
474
                foreach ($model as $endpoint) {
475
                    $dtos += self::extractSwaggerDefinition($endpoint);
476
                }
477
            }
478
            $formatted['definitions'] = $dtos;
479
            return $formatted;
480
        }
481
482
        /**
483
         * Method that extract the Dto class for the api documentation
484
         * @param string $dto
485
         * @param boolean $isArray
486
         *
487
         * @return string
488
         */
489
        protected function extractDtoName($dto, $isArray = false)
490
        {
491
            $dto = explode('\\', $dto);
492
            $modelDto = array_pop($dto) . "Dto";
493
            if ($isArray) {
494
                $modelDto .= "List";
495
            }
496
497
            return $modelDto;
498
        }
499
    }
500