Passed
Push — master ( e02e2f...bc1445 )
by Fran
03:07
created

DocumentorService::extractVisibility()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
483
                "type" => $type,
484
                "required" => $info['required'],
485
            ];
486
            $definition[$name]['properties'][$field] = [
487
                "type" => $type,
488
                "required" => $info['required'],
489
            ];
490
            if (strlen($format)) {
491
                $definition[$name]['properties'][$field]['format'] = $format;
492
            }
493
        }
494
        return $definition;
495
    }
496
497
    /**
498
     * @return array
499
     */
500
    private static function swaggerResponses() {
501
        $codes = [200, 400, 404, 500];
502
        $responses = [];
503
        foreach($codes as $code) {
504
            switch($code) {
505
                default:
506
                case 200:
507
                    $message = _('Successful response');
508
                    break;
509
                case 400:
510
                    $message = _('Client error in request');
511
                    break;
512
                case 404:
513
                    $message = _('Service not found');
514
                    break;
515
                case 500:
516
                    $message = _('Server error');
517
                    break;
518
            }
519
            $responses[$code] = [
520
                'description' => $message,
521
                'schema' => [
522
                    'type' => 'object',
523
                    'properties' => [
524
                        'success' => [
525
                            'type' => 'boolean'
526
                        ],
527
                        'data' => [
528
                            'type' => 'boolean',
529
                        ],
530
                        'total' => [
531
                            'type' => 'integer',
532
                            'format' => 'int32',
533
                        ],
534
                        'pages' => [
535
                            'type' => 'integer',
536
                            'format' => 'int32',
537
                        ]
538
                    ]
539
                ]
540
            ];
541
        }
542
        return $responses;
543
    }
544
545
    /**
546
     * Method that export
547
     * @param array $module
548
     *
549
     * @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...
550
     */
551
    public static function swaggerFormatter(array $module)
552
    {
553
        $formatted = [
554
            "swagger" => "2.0",
555
            "host" => preg_replace('/^(http|https)\:\/\/(.*)\/$/i', '$2', Router::getInstance()->getRoute('', true)),
556
            "basePath" => '/' . $module['name'] . '/api',
557
            "schemes" => [Request::getInstance()->getServer('HTTPS') == 'on' ? "https" : "http"],
558
            "info" => [
559
                "title" => _('Documentación API módulo ') . $module['name'],
560
                "version" => Config::getParam('api.version', '1.0'),
561
                "contact" => [
562
                    "name" => Config::getParam("author", "Fran López"),
563
                    "email" => Config::getParam("author_email", "[email protected]"),
564
                ]
565
            ]
566
        ];
567
        $dtos = $paths = [];
568
        $endpoints = DocumentorService::getInstance()->extractApiEndpoints($module);
569
        foreach ($endpoints as $model) {
570
            foreach ($model as $endpoint) {
571
                if(!preg_match('/^\/(admin|api)\//i', $endpoint['url']) && strlen($endpoint['url'])) {
572
                    $url = preg_replace('/\/'.$module['name'].'\/api/i', '', $endpoint['url']);
573
                    $description = $endpoint['description'];
574
                    $method = strtolower($endpoint['method']);
575
                    $paths[$url][$method] = [
576
                        'summary' => $description,
577
                        'produces' => ['application/json'],
578
                        'consumes' => ['application/json'],
579
                        'responses' => self::swaggerResponses(),
580
                        'parameters' => [],
581
                    ];
582
                    if(array_key_exists('parameters', $endpoint)) {
583
                        foreach($endpoint['parameters'] as $parameter => $type) {
584
                            list($type, $format) = self::translateSwaggerFormats($type);
585
                            $paths[$url][$method]['parameters'][] = [
586
                                'in' => 'path',
587
                                'required' => true,
588
                                'name' => $parameter,
589
                                'type' => $type,
590
                                'format' => $format,
591
                            ];
592
                        }
593
                    }
594
                    if(array_key_exists('query', $endpoint)) {
595
                        foreach($endpoint['query'] as $query) {
596
                            $paths[$url][$method]['parameters'][] = $query;
597
                        }
598
                    }
599
                    foreach($endpoint['objects'] as $name => $object) {
600
                        if(class_exists($name)) {
601
                            $class = GeneratorHelper::extractClassFromNamespace($name);
602
                            $classDefinition = [
603
                                'type' => 'object',
604
                                '$ref' => '#/definitions/' . $class,
605
                            ];
606
                            $paths[$url][$method]['responses'][200]['schema']['properties']['data'] = $classDefinition;
607
                            $dtos += self::extractSwaggerDefinition($class, $object);
608 View Code Duplication
                            if(!isset($paths[$url][$method]['tags']) || !in_array($class, $paths[$url][$method]['tags'])) {
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...
609
                                $paths[$url][$method]['tags'][] = $class;
610
                            }
611
                            if(array_key_exists('payload', $endpoint)) {
612
                                $paths[$url][$method]['parameters'][] = [
613
                                    'in' => 'body',
614
                                    'name' => $class,
615
                                    'required' => true,
616
                                    'schema' => $classDefinition
617
                                ];
618
                            }
619 View Code Duplication
                        } else {
620
                            if(!isset($paths[$url][$method]['tags']) || !in_array($endpoint['class'], $paths[$url][$method]['tags'])) {
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 135 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...
621
                                $paths[$url][$method]['tags'][] = $endpoint['class'];
622
                            }
623
                        }
624
                    }
625
                }
626
            }
627
        }
628
        $formatted['definitions'] = $dtos;
629
        $formatted['paths'] = $paths;
630
        return $formatted;
631
    }
632
633
    /**
634
     * Method that extract the Dto class for the api documentation
635
     * @param string $dto
636
     * @param boolean $isArray
637
     *
638
     * @return string
639
     */
640
    protected function extractDtoName($dto, $isArray = false)
641
    {
642
        $dto = explode('\\', $dto);
643
        $modelDto = array_pop($dto) . "Dto";
644
        if ($isArray) {
645
            $modelDto .= "List";
646
        }
647
648
        return $modelDto;
649
    }
650
}
651