Passed
Push — master ( 1b381e...137834 )
by Fran
03:04
created

DocumentorService::checkDtoAttributes()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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