Passed
Push — master ( 8b694f...0361bf )
by Fran
08:08
created

DocumentorService::getModules()   B

Complexity

Conditions 6
Paths 2

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 2
nop 1
dl 0
loc 22
ccs 0
cts 12
cp 0
crap 42
rs 8.6737
c 0
b 0
f 0
1
<?php
2
3
namespace PSFS\services;
4
5
use Propel\Runtime\Map\ColumnMap;
6
use PSFS\base\config\Config;
7
use PSFS\base\Logger;
8
use PSFS\base\Request;
9
use PSFS\base\Router;
10
use PSFS\base\Service;
11
use PSFS\base\types\helpers\DocumentorHelper;
12
use PSFS\base\types\helpers\GeneratorHelper;
13
use PSFS\base\types\helpers\I18nHelper;
14
use PSFS\base\types\helpers\InjectorHelper;
15
use PSFS\base\types\helpers\RouterHelper;
16
use Symfony\Component\Finder\Finder;
17
18
/**
19
 * Class DocumentorService
20
 * @package PSFS\services
21
 */
22
class DocumentorService extends Service
23
{
24
    public static $nativeMethods = [
25
        'modelList', // Api list
26
        'get', // Api get
27
        'post', // Api post
28
        'put', // Api put
29
        'delete', // Api delete
30
    ];
31
32
    const DTO_INTERFACE = '\\PSFS\\base\\dto\\Dto';
33
    const MODEL_INTERFACE = '\\Propel\\Runtime\\ActiveRecord\\ActiveRecordInterface';
34
35
    private $classes = [];
0 ignored issues
show
Unused Code introduced by
The property $classes is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
36
37
    /**
38
     * @Injectable
39
     * @var \PSFS\base\Router route
40
     */
41
    protected $route;
42
43
44
    /**
45
     * Method that extract all modules
46
     * @param string $requestModule
47
     * @return array
48
     */
49
    public function getModules($requestModule)
50
    {
51
        $modules = [];
52
        $domains = $this->route->getDomains();
53
        if (count($domains)) {
54
            foreach ($domains as $module => $info) {
55
                try {
56
                    $module = preg_replace('/(@|\/)/', '', $module);
57
                    if (!preg_match('/^ROOT/i', $module) && $module == $requestModule) {
58
                        $modules = [
59
                            'name' => $module,
60
                            'path' => realpath($info['template'] . DIRECTORY_SEPARATOR . '..'),
61
                        ];
62
                    }
63
                } catch (\Exception $e) {
64
                    $modules[] = $e->getMessage();
65
                }
66
            }
67
        }
68
69
        return $modules;
70
    }
71
72
    /**
73
     * Method that extract all endpoints for each module
74
     *
75
     * @param array $module
76
     *
77
     * @return array
78
     */
79
    public function extractApiEndpoints(array $module)
80
    {
81
        $module_path = $module['path'] . DIRECTORY_SEPARATOR . 'Api';
82
        $module_name = $module['name'];
83
        $endpoints = [];
84
        if (file_exists($module_path)) {
85
            $finder = new Finder();
86
            $finder->files()->in($module_path)->depth(0)->name('*.php');
87
            if (count($finder)) {
88
                /** @var \SplFileInfo $file */
89
                foreach ($finder as $file) {
90
                    $namespace = "\\{$module_name}\\Api\\" . str_replace('.php', '', $file->getFilename());
91
                    $info = $this->extractApiInfo($namespace, $module_name);
92
                    if (!empty($info)) {
93
                        $endpoints[$namespace] = $info;
94
                    }
95
                }
96
            }
97
        }
98
        return $endpoints;
99
    }
100
101
    /**
102
     * Method that extract all the endpoit information by reflection
103
     *
104
     * @param string $namespace
105
     * @param string $module
106
     * @return array
107
     */
108
    public function extractApiInfo($namespace, $module)
109
    {
110
        $info = [];
111
        if (Router::exists($namespace) && !I18nHelper::checkI18Class($namespace)) {
112
            $reflection = new \ReflectionClass($namespace);
113
            $visible = InjectorHelper::checkIsVisible($reflection->getDocComment());
114
            if($visible) {
115
                foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
116
                    try {
117
                        $mInfo = $this->extractMethodInfo($namespace, $method, $reflection, $module);
118
                        if (NULL !== $mInfo) {
119
                            $info[] = $mInfo;
120
                        }
121
                    } catch (\Exception $e) {
122
                        Logger::getInstance()->errorLog($e->getMessage());
123
                    }
124
                }
125
            }
126
        }
127
        return $info;
128
    }
129
130
    /**
131
     * Extract route from doc comments
132
     *
133
     * @param string $comments
134
     *
135
     * @return string
136
     */
137
    protected function extractRoute($comments = '')
138
    {
139
        $route = '';
140
        preg_match('/@route\ (.*)\n/i', $comments, $route);
141
142
        return $route[1];
143
    }
144
145
    /**
146
     * Extract api from doc comments
147
     *
148
     * @param string $comments
149
     *
150
     * @return string
151
     */
152
    protected function extractApi($comments = '')
153
    {
154
        $api = '';
155
        preg_match('/@api\ (.*)\n/i', $comments, $api);
156
157
        return $api[1];
158
    }
159
160
    /**
161
     * Extract api from doc comments
162
     *
163
     * @param string $comments
164
     *
165
     * @return boolean
166
     */
167
    protected function checkDeprecated($comments = '')
168
    {
169
        return false != preg_match('/@deprecated\n/i', $comments);
170
    }
171
172
    /**
173
     * Extract visibility from doc comments
174
     *
175
     * @param string $comments
176
     *
177
     * @return boolean
178
     */
179
    protected function extractVisibility($comments = '')
180
    {
181
        $visible = TRUE;
182
        preg_match('/@visible\ (true|false)\n/i', $comments, $visibility);
183
        if (count($visibility)) {
184
            $visible = !('false' == $visibility[1]);
185
        }
186
187
        return $visible;
188
    }
189
190
    /**
191
     * Method that extract the description for the endpoint
192
     *
193
     * @param string $comments
194
     *
195
     * @return string
196
     */
197
    protected function extractDescription($comments = '')
198
    {
199
        $description = '';
200
        $docs = explode("\n", $comments);
201
        if (count($docs)) {
202
            foreach ($docs as &$doc) {
203 View Code Duplication
                if (!preg_match('/(\*\*|\@)/i', $doc) && preg_match('/\*\ /i', $doc)) {
204
                    $doc = explode('* ', $doc);
205
                    $description = $doc[1];
206
                }
207
            }
208
        }
209
210
        return $description;
211
    }
212
213
    /**
214
     * Method that extract the type of a variable
215
     *
216
     * @param string $comments
217
     *
218
     * @return string
219
     */
220
    public static function extractVarType($comments = '')
221
    {
222
        $type = 'string';
223
        preg_match('/@var\ (.*) (.*)\n/i', $comments, $varType);
224
        if (count($varType)) {
225
            $aux = trim($varType[1]);
226
            $type = str_replace(' ', '', strlen($aux) > 0 ? $varType[1] : $varType[2]);
227
        }
228
229
        return $type;
230
    }
231
232
    /**
233
     * Method that extract the payload for the endpoint
234
     *
235
     * @param string $model
236
     * @param string $comments
237
     *
238
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string|array|boolean>.

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...
239
     */
240
    protected function extractPayload($model, $comments = '')
241
    {
242
        $payload = [];
243
        preg_match('/@payload\ (.*)\n/i', $comments, $doc);
244
        $isArray = false;
245
        if (count($doc)) {
246
            $namespace = str_replace('{__API__}', $model, $doc[1]);
247 View Code Duplication
            if (false !== strpos($namespace, '[') && false !== strpos($namespace, ']')) {
248
                $namespace = str_replace(']', '', str_replace('[', '', $namespace));
249
                $isArray = true;
250
            }
251
            $payload = $this->extractModelFields($namespace);
252
            $reflector = new \ReflectionClass($namespace);
253
            $namespace = $reflector->getShortName();
254
        } else {
255
            $namespace = $model;
256
        }
257
258
        return [$namespace, $payload, $isArray];
259
    }
260
261
    /**
262
     * Extract all the properties from Dto class
263
     *
264
     * @param string $class
265
     *
266
     * @return array
267
     */
268
    protected function extractDtoProperties($class)
269
    {
270
        $properties = [];
271
        $reflector = new \ReflectionClass($class);
272
        if ($reflector->isSubclassOf(self::DTO_INTERFACE)) {
273
            $properties = array_merge($properties, InjectorHelper::extractVariables($reflector));
274
        }
275
276
        return $properties;
277
    }
278
279
    /**
280
     * Extract return class for api endpoint
281
     *
282
     * @param string $model
283
     * @param string $comments
284
     *
285
     * @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...
286
     */
287
    protected function extractReturn($model, $comments = '')
288
    {
289
        $modelDto = [];
290
        preg_match('/\@return\ (.*)\((.*)\)\n/i', $comments, $returnTypes);
291
        if (count($returnTypes)) {
292
            // Extract principal DTO information
293
            if (array_key_exists(1, $returnTypes)) {
294
                $modelDto = $this->extractDtoProperties($returnTypes[1]);
295
            }
296
            if (array_key_exists(2, $returnTypes)) {
297
                $subDtos = preg_split('/,?\ /', str_replace('{__API__}', $model, $returnTypes[2]));
298
                if (count($subDtos)) {
299
                    foreach ($subDtos as $subDto) {
300
                        $isArray = false;
301
                        list($field, $dtoName) = explode('=', $subDto);
302 View Code Duplication
                        if (false !== strpos($dtoName, '[') && false !== strpos($dtoName, ']')) {
303
                            $dtoName = str_replace(']', '', str_replace('[', '', $dtoName));
304
                            $isArray = true;
305
                        }
306
                        $dto = $this->extractModelFields($dtoName);
307
                        $modelDto[$field] = ($isArray) ? [$dto] : $dto;
308
                        $modelDto['objects'][$dtoName] = $dto;
309
                    }
310
                }
311
            }
312
        }
313
314
        return $modelDto;
315
    }
316
317
    /**
318
     * Extract all fields from a ActiveResource model
319
     *
320
     * @param string $namespace
321
     *
322
     * @return mixed
323
     */
324
    protected function extractModelFields($namespace)
325
    {
326
        $payload = [];
327
        try {
328
            $reflector = new \ReflectionClass($namespace);
329
            // Checks if reflector is a subclass of propel ActiveRecords
330
            if (NULL !== $reflector && $reflector->isSubclassOf(self::MODEL_INTERFACE)) {
331
                $tableMap = $namespace::TABLE_MAP;
332
                $tableMap = $tableMap::getTableMap();
333
                /** @var ColumnMap $field */
334
                foreach ($tableMap->getColumns() as $field) {
335
                    list($type, $format) = DocumentorHelper::translateSwaggerFormats($field->getType());
336
                    $info = [
337
                        "type" => $type,
338
                        "required" => $field->isNotNull(),
339
                        'format' => $format,
340
                    ];
341
                    if(count($field->getValueSet())) {
342
                        $info['enum'] = array_values($field->getValueSet());
343
                    }
344
                    if(null !== $field->getDefaultValue()) {
345
                        $info['default'] = $field->getDefaultValue();
346
                    }
347
                    $payload[$field->getPhpName()] = $info;
348
                }
349
            } elseif (null !== $reflector && $reflector->isSubclassOf(self::DTO_INTERFACE)) {
350
                $payload = $this->extractDtoProperties($namespace);
351
            }
352
        } catch (\Exception $e) {
353
            Logger::getInstance()->errorLog($e->getMessage());
354
        }
355
356
        return $payload;
357
    }
358
359
    /**
360
     * Method that extract all the needed info for each method in each API
361
     *
362
     * @param string $namespace
363
     * @param \ReflectionMethod $method
364
     * @param \ReflectionClass $reflection
365
     * @param string $module
366
     *
367
     * @return array
368
     */
369
    protected function extractMethodInfo($namespace, \ReflectionMethod $method, \ReflectionClass $reflection, $module)
370
    {
371
        $methodInfo = NULL;
372
        $docComments = $method->getDocComment();
373
        if (FALSE !== $docComments && preg_match('/\@route\ /i', $docComments)) {
374
            $api = self::extractApi($reflection->getDocComment());
375
            list($route, $info) = RouterHelper::extractRouteInfo($method, $api, $module);
376
            $route = explode('#|#', $route);
377
            $modelNamespace = str_replace('Api', 'Models', $namespace);
378
            if ($info['visible'] && !self::checkDeprecated($docComments)) {
379
                try {
380
                    $return = $this->extractReturn($modelNamespace, $docComments);
381
                    $url = array_pop($route);
382
                    $methodInfo = [
383
                        'url' => str_replace("/" . $module . "/api", '', $url),
384
                        'method' => $info['http'],
385
                        'description' => $info['label'],
386
                        'return' => $return,
387
                        'objects' => $return['objects'],
388
                        'class' => $reflection->getShortName(),
389
                    ];
390
                    unset($methodInfo['return']['objects']);
391
                    $this->setRequestParams($method, $methodInfo, $modelNamespace, $docComments);
392
                    $this->setQueryParams($method, $methodInfo);
393
                    $this->setRequestHeaders($reflection, $methodInfo);
394
                } catch (\Exception $e) {
395
                    Logger::getInstance()->errorLog($e->getMessage());
396
                }
397
            }
398
        }
399
400
        return $methodInfo;
401
    }
402
403
    /**
404
     * @return array
405
     */
406
    private static function swaggerResponses()
407
    {
408
        $codes = [200, 400, 404, 500];
409
        $responses = [];
410
        foreach ($codes as $code) {
411
            switch ($code) {
412
                default:
413
                case 200:
414
                    $message = _('Successful response');
415
                    break;
416
                case 400:
417
                    $message = _('Client error in request');
418
                    break;
419
                case 404:
420
                    $message = _('Service not found');
421
                    break;
422
                case 500:
423
                    $message = _('Server error');
424
                    break;
425
            }
426
            $responses[$code] = [
427
                'description' => $message,
428
                'schema' => [
429
                    'type' => 'object',
430
                    'properties' => [
431
                        'success' => [
432
                            'type' => 'boolean'
433
                        ],
434
                        'data' => [
435
                            'type' => 'boolean',
436
                        ],
437
                        'total' => [
438
                            'type' => 'integer',
439
                            'format' => 'int32',
440
                        ],
441
                        'pages' => [
442
                            'type' => 'integer',
443
                            'format' => 'int32',
444
                        ]
445
                    ]
446
                ]
447
            ];
448
        }
449
        return $responses;
450
    }
451
452
    /**
453
     * Method that export
454
     * @param array $module
455
     *
456
     * @return array
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...
457
     */
458
    public static function swaggerFormatter(array $module)
459
    {
460
        $formatted = [
461
            "swagger" => "2.0",
462
            "host" => preg_replace('/^(http|https)\:\/\/(.*)\/$/i', '$2', Router::getInstance()->getRoute('', true)),
463
            "basePath" => '/' . $module['name'] . '/api',
464
            "schemes" => [Request::getInstance()->getServer('HTTPS') == 'on' ? "https" : "http"],
465
            "info" => [
466
                "title" => _('Documentación API módulo ') . $module['name'],
467
                "version" => Config::getParam('api.version', '1.0.0'),
468
                "contact" => [
469
                    "name" => Config::getParam("author", "Fran López"),
470
                    "email" => Config::getParam("author.email", "[email protected]"),
471
                ]
472
            ]
473
        ];
474
        $dtos = $paths = [];
475
        $endpoints = DocumentorService::getInstance()->extractApiEndpoints($module);
476
        foreach ($endpoints as $model) {
477
            foreach ($model as $endpoint) {
478
                if (!preg_match('/^\/(admin|api)\//i', $endpoint['url']) && strlen($endpoint['url'])) {
479
                    $url = preg_replace('/\/' . $module['name'] . '\/api/i', '', $endpoint['url']);
480
                    $description = $endpoint['description'];
481
                    $method = strtolower($endpoint['method']);
482
                    $paths[$url][$method] = [
483
                        'summary' => $description,
484
                        'produces' => ['application/json'],
485
                        'consumes' => ['application/json'],
486
                        'responses' => self::swaggerResponses(),
487
                        'parameters' => [],
488
                    ];
489
                    if (array_key_exists('parameters', $endpoint)) {
490
                        foreach ($endpoint['parameters'] as $parameter => $type) {
491
                            list($type, $format) = DocumentorHelper::translateSwaggerFormats($type);
492
                            $paths[$url][$method]['parameters'][] = [
493
                                'in' => 'path',
494
                                'required' => true,
495
                                'name' => $parameter,
496
                                'type' => $type,
497
                                'format' => $format,
498
                            ];
499
                        }
500
                    }
501 View Code Duplication
                    if (array_key_exists('query', $endpoint)) {
502
                        foreach ($endpoint['query'] as $query) {
503
                            $paths[$url][$method]['parameters'][] = $query;
504
                        }
505
                    }
506 View Code Duplication
                    if (array_key_exists('headers', $endpoint)) {
507
                        foreach ($endpoint['headers'] as $query) {
508
                            $paths[$url][$method]['parameters'][] = $query;
509
                        }
510
                    }
511
                    foreach ($endpoint['objects'] as $name => $object) {
512
                        DocumentorHelper::parseObjects($paths, $dtos, $name, $endpoint, $object, $url, $method);
513
                    }
514
                }
515
            }
516
        }
517
        ksort($dtos);
518
        uasort($paths, function($path1, $path2) {
519
            $key1 = array_keys($path1)[0];
520
            $key2 = array_keys($path2)[0];
521
            return strcmp($path1[$key1]['tags'][0], $path2[$key2]['tags'][0]);
522
        });
523
        $formatted['definitions'] = $dtos;
524
        $formatted['paths'] = $paths;
525
        return $formatted;
526
    }
527
528
    /**
529
     * Method that extract the Dto class for the api documentation
530
     * @param string $dto
531
     * @param boolean $isArray
532
     *
533
     * @return string
534
     */
535
    protected function extractDtoName($dto, $isArray = false)
536
    {
537
        $dto = explode('\\', $dto);
538
        $modelDto = array_pop($dto) . "Dto";
539
        if ($isArray) {
540
            $modelDto .= "List";
541
        }
542
543
        return $modelDto;
544
    }
545
546
    /**
547
     * @param \ReflectionMethod $method
548
     * @param $methodInfo
549
     */
550
    protected function setQueryParams(\ReflectionMethod $method, &$methodInfo)
551
    {
552
        if (in_array($methodInfo['method'], ['GET']) && in_array($method->getShortName(), self::$nativeMethods)) {
553
            $methodInfo['query'] = [];
554
            $methodInfo['query'][] = [
555
                "name" => "__limit",
556
                "in" => "query",
557
                "description" => _("Límite de registros a devolver, -1 para devolver todos los registros"),
558
                "required" => false,
559
                "type" => "integer",
560
            ];
561
            $methodInfo['query'][] = [
562
                "name" => "__page",
563
                "in" => "query",
564
                "description" => _("Página a devolver"),
565
                "required" => false,
566
                "type" => "integer",
567
            ];
568
            $methodInfo['query'][] = [
569
                "name" => "__fields",
570
                "in" => "query",
571
                "description" => _("Campos a devolver"),
572
                "required" => false,
573
                "type" => "array",
574
                "items" => [
575
                    "type" => "string",
576
                ]
577
            ];
578
        }
579
    }
580
    /**
581
     * @param \ReflectionClass $reflection
582
     * @param $methodInfo
583
     */
584
    protected function setRequestHeaders(\ReflectionClass $reflection, &$methodInfo)
585
    {
586
587
        $methodInfo['headers'] = [];
588
        foreach($reflection->getProperties() as $property) {
589
            $doc = $property->getDocComment();
590
            preg_match('/@header\ (.*)\n/i', $doc, $headers);
591
            if(count($headers)) {
592
                $header = [
593
                    "name" => $headers[1],
594
                    "in" => "header",
595
                    "required" => true,
596
                ];
597
598
                // Extract var type
599
                $header['type'] = $this->extractVarType($doc);
600
601
                // Extract description
602
                preg_match('/@label\ (.*)\n/i', $doc, $label);
603
                if(count($label)) {
604
                    $header['description'] = _($label[1]);
605
                }
606
607
                // Extract default value
608
                preg_match('/@default\ (.*)\n/i', $doc, $default);
609
                if(count($default)) {
610
                    $header['default'] = $default[1];
611
                }
612
                $methodInfo['headers'][] = $header;
613
            }
614
        }
615
    }
616
617
    /**
618
     * @param \ReflectionMethod $method
619
     * @param array $methodInfo
620
     * @param string $modelNamespace
621
     * @param string $docComments
622
     */
623
    protected function setRequestParams(\ReflectionMethod $method, &$methodInfo, $modelNamespace, $docComments)
624
    {
625
        if (in_array($methodInfo['method'], ['POST', 'PUT'])) {
626
            list($payloadNamespace, $payloadDto, $isArray) = $this->extractPayload($modelNamespace, $docComments);
627
            if (count($payloadDto)) {
628
                $methodInfo['payload'] = [
629
                    'type' => $payloadNamespace,
630
                    'properties' => $payloadDto,
631
                    'is_array' => $isArray,
632
                ];
633
            }
634
        }
635
        if ($method->getNumberOfParameters() > 0) {
636
            $methodInfo['parameters'] = [];
637
            foreach ($method->getParameters() as $parameter) {
638
                $parameterName = $parameter->getName();
639
                $types = [];
640
                preg_match_all('/\@param\ (.*)\ \$' . $parameterName . '$/im', $docComments, $types);
641
                if (count($types) > 1) {
642
                    $methodInfo['parameters'][$parameterName] = $types[1][0];
643
                }
644
            }
645
        }
646
    }
647
}
648