Completed
Push — master ( b819b4...8115f1 )
by Sherif
14:17
created

GenerateDocCommand::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace App\Modules\Core\Console\Commands;
4
5
use Illuminate\Console\Command;
6
use Illuminate\Support\Arr;
7
use App\Modules\Reporting\Services\ReportService;
8
9
class GenerateDocCommand extends Command
10
{
11
    /**
12
     * The name and signature of the console command.
13
     *
14
     * @var string
15
     */
16
    protected $signature = 'doc:generate';
17
18
    /**
19
     * The console command description.
20
     *
21
     * @var string
22
     */
23
    protected $description = 'Generate api documentation';
24
25
    /**
26
     * @var ReprotService
27
     */
28
    protected $reportService;
29
30
    /**
31
     * Init new object.
32
     *
33
     * @return  void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
34
     */
35
    public function __construct(ReportService $reportService)
36
    {
37
        $this->reportService = $reportService;
0 ignored issues
show
Documentation Bug introduced by
It seems like $reportService of type object<App\Modules\Repor...Services\ReportService> is incompatible with the declared type object<App\Modules\Core\...Commands\ReprotService> of property $reportService.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
38
        parent::__construct();
39
    }
40
41
    /**
42
     * Execute the console command.
43
     *
44
     * @return mixed
45
     */
46
    public function handle()
47
    {
48
        $docData           = [];
49
        $docData['models'] = [];
50
        $routes            = $this->getRoutes();
51
        foreach ($routes as $route) {
52
            if ($route) {
53
                $actoinArray = explode('@', $route['action']);
54
                if (Arr::get($actoinArray, 1, false)) {
55
                    $prefix = $route['prefix'];
56
                    $module = \Str::camel(str_replace('/', '_', str_replace('api', '', $prefix)));
57
                    if ($prefix === 'telescope') {
58
                        continue;
59
                    }
60
61
                    $controller       = $actoinArray[0];
62
                    $method           = $actoinArray[1];
63
                    $route['name']    = $method;
64
                    $reflectionClass  = new \ReflectionClass($controller);
65
                    $reflectionMethod = $reflectionClass->getMethod($method);
66
                    $classProperties  = $reflectionClass->getDefaultProperties();
67
                    $skipLoginCheck   = Arr::get($classProperties, 'skipLoginCheck', false);
68
                    $modelName        = explode('\\', $controller);
69
                    $modelName        = lcfirst(str_replace('Controller', '', end($modelName)));
70
71
                    $this->processDocBlock($route, $reflectionMethod);
72
                    $this->getHeaders($route, $method, $skipLoginCheck);
73
                    $this->getPostData($route, $reflectionMethod, explode('\\', $reflectionClass->getName())[2], $modelName);
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
74
75
                    $route['response'] = $this->getResponseObject($modelName, $route['name'], $route['returnDocBlock']);
76
                    $docData['modules'][$module][] = $route;
77
78
                    $this->getModels($modelName, $docData, $reflectionClass);
79
                }
80
            }
81
        }
82
        
83
        $docData['errors']  = $this->getErrors();
84
        $docData['reports'] = $this->reportService->all();
85
        \File::put(app_path('Modules/Core/Resources/api.json'), json_encode($docData));
86
    }
87
88
    /**
89
     * Get list of all registered routes.
90
     *
91
     * @return collection
92
     */
93
    protected function getRoutes()
94
    {
95
        return collect(\Route::getRoutes())->map(function ($route) {
96
            if (strpos($route->uri(), 'api/') !== false) {
97
                return [
98
                    'method' => $route->methods()[0],
99
                    'uri'    => $route->uri(),
100
                    'action' => $route->getActionName(),
101
                    'prefix' => $route->getPrefix()
102
                ];
103
            }
104
            return false;
105
        })->all();
106
    }
107
108
    /**
109
     * Generate headers for the given route.
110
     *
111
     * @param  array  &$route
112
     * @param  string $method
113
     * @param  array  $skipLoginCheck
114
     * @return void
115
     */
116
    protected function getHeaders(&$route, $method, $skipLoginCheck)
117
    {
118
        $route['headers'] = [
119
        'Accept'         => 'application/json',
120
        'Content-Type'   => 'application/json',
121
        'Accept-Language' => 'The language of the returned data: ar, en or all.',
122
        'time-zone'       => 'Your locale time zone',
123
        ];
124
125
126
        if (! $skipLoginCheck || ! in_array($method, $skipLoginCheck)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $skipLoginCheck of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
127
            $route['headers']['Authorization'] = 'Bearer {token}';
128
        }
129
    }
130
131
    /**
132
     * Generate description and params for the given route
133
     * based on the docblock.
134
     *
135
     * @param  array  &$route
136
     * @param  \ReflectionMethod $reflectionMethod
137
     * @return void
138
     */
139
    protected function processDocBlock(&$route, $reflectionMethod)
140
    {
141
        $factory                 = \phpDocumentor\Reflection\DocBlockFactory::createInstance();
142
        $docblock                = $factory->create($reflectionMethod->getDocComment());
143
        $route['description']    = trim(preg_replace('/\s+/', ' ', $docblock->getSummary()));
144
        $params                  = $docblock->getTagsByName('param');
145
        $route['returnDocBlock'] = $docblock->getTagsByName('return')[0]->getType()->getFqsen()->getName();
146
147
        foreach ($params as $param) {
148
            $name = $param->getVariableName();
149
            if ($name == 'perPage') {
150
                $route['parametars'][$name] = 'perPage? number of records per page default 15';
151
            } elseif ($name == 'sortBy') {
152
                $route['parametars'][$name] = 'sortBy? sort column default created_at';
153
            } elseif ($name == 'desc') {
154
                $route['parametars'][$name] = 'desc? sort descending or ascending default is descending';
155
            } elseif ($name !== 'request') {
156
                $route['parametars'][$name] = $param->getDescription()->render();
157
            }
158
        }
159
160
        if ($route['name'] === 'index') {
161
            $route['parametars']['perPage'] = 'perPage? number of records per page default 15';
162
            $route['parametars']['sortBy']  = 'sortBy? sort column default created_at';
163
            $route['parametars']['desc']    = 'desc? sort descending or ascending default is descending';
164
            $route['parametars']['trashed'] = 'trashed? retreive trashed or not default not';
165
        }
166
    }
167
168
    /**
169
     * Generate post body for the given route.
170
     *
171
     * @param  array  &$route
172
     * @param  \ReflectionMethod $reflectionMethod
173
     * @param  string $module
174
     * @param  string $modelName
175
     * @return void
176
     */
177
    protected function getPostData(&$route, $reflectionMethod, $module, $modelName)
178
    {
179
        $parameters = $reflectionMethod->getParameters();
180
        $className = optional(optional(\Arr::get($parameters, 0))->getType())->getName();
181
        if ($className) {
182
            $reflectionClass  = new \ReflectionClass($className);
183
        } elseif (in_array($reflectionMethod->getName(), ['store', 'update'])) {
0 ignored issues
show
Bug introduced by
Consider using $reflectionMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
184
            $className = 'App\\Modules\\' . ucfirst($module) . '\\Http\\Requests\\Store'  . ucfirst($modelName);
185
            $reflectionClass  = new \ReflectionClass($className);
186
        }
187
188
        if (isset($reflectionClass) && $reflectionClass->hasMethod('rules')) {
189
            $reflectionMethod = $reflectionClass->getMethod('rules');
190
            $route['body'] = $reflectionMethod->invoke(new $className);
191
192
            foreach ($route['body'] as &$rule) {
193
                if (strpos($rule, 'unique')) {
194
                    $rule = substr($rule, 0, strpos($rule, 'unique') + 6);
195
                } elseif (strpos($rule, 'exists')) {
196
                    $rule = substr($rule, 0, strpos($rule, 'exists') - 1);
197
                }
198
            }
199
        }
200
    }
201
202
    /**
203
     * Generate application errors.
204
     *
205
     * @return array
206
     */
207
    protected function getErrors()
208
    {
209
        $errors = [];
210
        foreach (\Module::all() as $module) {
211
            $nameSpace = 'App\\Modules\\' . $module['basename'];
212
            $class = $nameSpace . '\\Errors\\'  . $module['basename'] . 'Errors';
213
            $reflectionClass = new \ReflectionClass($class);
214
            foreach ($reflectionClass->getMethods() as $method) {
215
                $methodName       = $method->name;
216
                $reflectionMethod = $reflectionClass->getMethod($methodName);
217
                $body             = $this->getMethodBody($reflectionMethod);
218
219
                preg_match('/abort\(([^#]+)\,/iU', $body, $match);
220
221
                if (count($match)) {
222
                    $errors[$match[1]][] = $methodName;
223
                }
224
            }
225
        }
226
227
        return $errors;
228
    }
229
230
    /**
231
     * Get the given method body code.
232
     *
233
     * @param  object $reflectionMethod
234
     * @return string
235
     */
236
    protected function getMethodBody($reflectionMethod)
237
    {
238
        $filename   = $reflectionMethod->getFileName();
239
        $start_line = $reflectionMethod->getStartLine() - 1;
240
        $end_line   = $reflectionMethod->getEndLine();
241
        $length     = $end_line - $start_line;
242
        $source     = file($filename);
243
        $body       = implode("", array_slice($source, $start_line, $length));
244
        $body       = trim(preg_replace('/\s+/', '', $body));
245
246
        return $body;
247
    }
248
249
    /**
250
     * Get example object of all availble models.
251
     *
252
     * @param  string $modelName
253
     * @param  array  $docData
254
     * @return string
255
     */
256
    protected function getModels($modelName, &$docData, $reflectionClass)
257
    {
258
        if ($modelName && ! Arr::has($docData['models'], $modelName)) {
259
            $repo = call_user_func_array("\Core::{$modelName}", []);
260
            if (! $repo) {
261
                return;
262
            }
263
            
264
            $modelClass = get_class($repo->model);
265
            $model      = factory($modelClass)->make();
266
267
            $property = $reflectionClass->getProperty('modelResource');
268
            $property->setAccessible(true);
269
            $modelResource = $property->getValue(\App::make($reflectionClass->getName()));
270
271
            $relations = [];
272
            $relationMethods = ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'morphToMany', 'morphTo'];
273
            $reflector = new \ReflectionClass($model);
274
            foreach ($reflector->getMethods() as $reflectionMethod) {
275
                $body = $this->getMethodBody($reflectionMethod);
276
                foreach ($relationMethods as $relationMethod) {
277
                    $relation = $reflectionMethod->getName();
0 ignored issues
show
Bug introduced by
Consider using $reflectionMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
278
                    if (strpos($body, '$this->' . $relationMethod) && $relation !== 'morphedByMany') {
279
                        $relations[] = $relation;
280
                        break;
281
                    }
282
                }
283
            }
284
285
            $modelResource = new $modelResource($model->load($relations));
286
            $modelArr      = $modelResource->toArray([]);
287
288
            foreach ($modelArr as $key => $attr) {
289
                if (is_object($attr) && property_exists($attr, 'resource') && $attr->resource instanceof \Illuminate\Http\Resources\MissingValue) {
290
                    unset($modelArr[$key]);
291
                }
292
            }
293
294
            $docData['models'][$modelName] = json_encode($modelArr, JSON_PRETTY_PRINT);
295
        }
296
    }
297
298
    /**
299
     * Get the route response object type.
300
     *
301
     * @param  string $modelName
302
     * @param  string $method
303
     * @param  string $returnDocBlock
304
     * @return array
305
     */
306
    protected function getResponseObject($modelName, $method, $returnDocBlock)
307
    {
308
        $relations = config('core.relations');
309
        $relations = Arr::has($relations, $modelName) ? Arr::has($relations[$modelName], $method) ? $relations[$modelName] : false : false;
310
        $modelName = call_user_func_array("\Core::{$returnDocBlock}", []) ? $returnDocBlock : $modelName;
311
312
        return $relations ? [$modelName => $relations && $relations[$method] ? $relations[$method] : []] : false;
313
    }
314
}
315