ApiDocs::show()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php 
2
3
namespace Yaro\ApiDocs;
4
5
use ReflectionClass;
6
use Illuminate\Routing\Router;
7
use Illuminate\Contracts\Config\Repository as Config;
8
use Illuminate\Http\Request;
9
use Yaro\ApiDocs\Blueprint;
10
11
class ApiDocs
12
{
13
    
14
    private $router;
15
    private $config;
16
    private $request;
17
    
18
    public function __construct(Router $router, Config $config, Request $request)
19
    {
20
        $this->router  = $router;
21
        $this->config  = $config;
22
        $this->request = $request;
23
    } // end __construct
24
25
    public function show($routePrefix = null)
26
    {
27
        $currentPrefix = $this->request->get('prefix', $this->getRoutePrefix($routePrefix));
28
        $endpoints = $this->getEndpoints($currentPrefix);
29
        $endpoints = $this->getSortedEndpoints($endpoints);
30
        $prefixes  = $this->getRoutePrefixes();
31
        
32
        return view('apidocs::docs', compact('endpoints', 'prefixes', 'currentPrefix'));
33
    } // end show
34
35
    public function blueprint($routePrefix = null)
36
    {
37
        $routePrefix = $this->request->get('prefix', $this->getRoutePrefix($routePrefix));
38
        
39
        $blueprint = app()->make(Blueprint::class);
40
        $blueprint->setRoutePrefix($routePrefix);
41
        $blueprint->setEndpoints($this->getEndpoints($routePrefix));
42
        
43
        return $blueprint;
44
    } // end blueprint
45
    
46
    private function getEndpoints($routePrefix)
47
    {
48
        $endpoints = [];
49
50
        foreach ($this->router->getRoutes() as $route) {
51
            if (!$this->isPrefixedRoute($route, $routePrefix) || $this->isClosureRoute($route) || $this->isExcluded($route)) {
52
                continue;
53
            }
54
            
55
            $actionController = explode("@", $this->getRouteParam($route, 'action.controller'));
56
            $class  = $actionController[0];
57
            $method = $actionController[1];
58
            
59
            if (!class_exists($class) || !method_exists($class, $method)) {
60
                continue;
61
            }
62
63
64
            list($title, $description, $params) = $this->getRouteDocBlock($class, $method);
65
            $key = $this->generateEndpointKey($class);
66
            
67
            $endpoints[$key][] = [
68
                'hash'    => $this->generateHashForUrl($key, $route, $method),
69
                'uri'     => $this->getRouteParam($route, 'uri'),
70
                'name'    => $method,
71
                'methods' => $this->getRouteParam($route, 'methods'),
72
                'docs' => [
73
                    'title'       => $title, 
74
                    'description' => trim($description), 
75
                    'params'      => $params,
76
                    'uri_params'  => $this->getUriParams($route),
77
                ],
78
            ];
79
        }
80
81
        return $endpoints;
82
    } // end getEndpoints
83
    
84
    private function isExcluded($route)
85
    {
86
        $uri = $this->getRouteParam($route, 'uri');
87
        $actionController = $this->getRouteParam($route, 'action.controller');
88
        
89
        return $this->isExcludedClass($actionController) || $this->isExcludedRoute($uri);
90
    } // end isExcluded
91
    
92
    private function isExcludedRoute($uri)
93
    {
94
        foreach ($this->config->get('yaro.apidocs.exclude.routes', []) as $pattern) {
95
            if (str_is($pattern, $uri)) {
96
                return true;
97
            }
98
        }
99
        
100
        return false;
101
    } // end isExcludedRoute
102
    
103
    private function isExcludedClass($actionController)
104
    {
105
        foreach ($this->config->get('yaro.apidocs.exclude.classes', []) as $pattern) {
106
            if (str_is($pattern, $actionController)) {
107
                return true;
108
            }
109
        }
110
        
111
        return false;
112
    } // end isExcludedClass
113
    
114
    private function isPrefixedRoute($route, $routePrefix)
115
    {
116
        $regexp = '~^'. preg_quote($routePrefix) .'~';
117
        
118
        return preg_match($regexp, $this->getRouteParam($route, 'uri'));
119
    } // end isPrefixedRoute
120
    
121
    private function getRoutePrefix($routePrefix)
122
    {
123
        $prefixes = $this->getRoutePrefixes();
124
        
125
        return in_array($routePrefix, $prefixes) ? $routePrefix : array_shift($prefixes);
126
    }
127
    
128
    private function getRoutePrefixes()
129
    {
130
        $prefixes = $this->config->get('yaro.apidocs.prefix', 'api');
131
        if (!is_array($prefixes)) {
132
            $prefixes = [$prefixes];
133
        }
134
        
135
        return $prefixes;
136
    }
137
    
138
    private function isClosureRoute($route)
139
    {
140
        $action = $this->getRouteParam($route, 'action.uses');
141
        
142
        return is_object($action);
143
    } // end isClosureRoute
144
    
145
    private function getRouteDocBlock($class, $method)
146
    {
147
        $reflector = new ReflectionClass($class);
148
149
        $title = implode(' ', $this->splitCamelCaseToWords($method));
150
        $title = ucfirst(strtolower($title));
151
        $description = '';
152
        $params = [];
153
154
        $reflectorMethod = $reflector->getMethod($method);
155
        $docs = explode("\n", $reflectorMethod->getDocComment());
156
        $docs = array_filter($docs);
157
        if (!$docs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $docs 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...
158
            return [$title, $description, $params];
159
        }
160
        
161
        $docs = $this->filterDocBlock($docs);
162
        
163
        $title = array_shift($docs);
164
        
165
        $checkForLongDescription = true;
166
        foreach ($docs as $line) {
167
            if ($checkForLongDescription && !preg_match('~^@\w+~', $line)) {
168
                $description .= trim($line) .' ';
169
            } elseif (preg_match('~^@\w+~', $line)) {
170
                $checkForLongDescription = false;
171
                if (preg_match('~^@param~', $line)) {
172
                    $paramChunks = $this->getParamChunksFromLine($line);
173
                    
174
                    $paramType = array_shift($paramChunks);
175
                    $paramName = substr(array_shift($paramChunks), 1);
176
                    $params[$paramName] = [
177
                        'type'        => $paramType,
178
                        'name'        => $paramName,
179
                        'description' => implode(' ', $paramChunks),
180
                        'template'    => $this->getParamTemplateByType($paramType),
181
                    ];
182
                }
183
            }
184
        }
185
186
        // TODO:
187
        $rules = [];
188
        foreach($reflectorMethod->getParameters() as $reflectorParam) {
189
            $paramClass = $reflectorParam->getClass();
190
            if ($paramClass instanceof ReflectionClass) {
191
                $name = $paramClass->getName();
0 ignored issues
show
Bug introduced by
Consider using $paramClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
192
                $paramClassInstance = new $name;
193
                if (is_a($paramClassInstance, Request::class) && method_exists($paramClassInstance, 'rules')) {
194
                    $paramClassInstance->__apidocs = true;
195
                    $rules = $paramClassInstance->rules();
196
                }
197
            }
198
        }
199
200
        $params = $this->fillParamsWithRequestRules($params, $rules);
201
202
        foreach ($params as $name => $param) {
203
            $params[$name]['rules'] = $rules[$name] ?? [];
204
            if (is_string($params[$name]['rules'])) {
205
                $params[$name]['rules'] = explode('|', $params[$name]['rules']);
206
            } elseif (is_array($params[$name]['rules'])) {
207
                $params[$name]['rules'] = $params[$name]['rules'];
208
            }
209
        }
210
211
        return [$title, $description, $params];
212
    } // end getRouteDocBlock
213
214
    private function fillParamsWithRequestRules($params, $rules)
215
    {
216
        foreach ($rules as $paramName => $rule) {
217
            if (isset($params[$paramName])) {
218
                continue;
219
            }
220
221
            $params[$paramName] = [
222
                'type'        => 'string',
223
                'name'        => $paramName,
224
                'description' => '',
225
                'template'    => $this->getParamTemplateByType('string'),
226
            ];
227
        }
228
229
        return $params;
230
    }
231
    
232
    private function getParamTemplateByType($paramType)
233
    {
234
        switch ($paramType) {
235
            case 'file':
236
                return 'file';
237
                
238
            case 'bool':
239
            case 'boolean':
240
                return 'boolean';
241
                
242
            case 'int':
243
                return 'integer';
244
                
245
            case 'text':
246
            case 'string':
247
            default:
248
                return 'string';
249
        }
250
    } // end getParamTemplateByType
251
    
252
    private function getParamChunksFromLine($line)
253
    {
254
        $paramChunks = explode(' ', $line);
255
        $paramChunks = array_filter($paramChunks, function($val) {
256
            return $val !== '';
257
        });
258
        unset($paramChunks[0]);
259
        
260
        return $paramChunks;
261
    } // end getParamChunksFromLine
262
    
263
    private function filterDocBlock($docs)
264
    {
265
        foreach ($docs as &$line) {
266
            $line = preg_replace('~\s*\*\s*~', '', $line);
267
            $line = preg_replace('~^/$~', '', $line);
268
        }
269
        $docs = array_values(array_filter($docs));
270
        
271
        return $docs;
272
    } // end filterDocBlock
273
    
274
    private function generateEndpointKey($class)
275
    {
276
        $disabledNamespaces = $this->config->get('yaro.apidocs.disabled_namespaces', []);
277
        
278
        $chunks = explode('\\', $class);
279
        foreach ($chunks as $index => $chunk) {
280
            if (in_array($chunk, $disabledNamespaces)) {
281
                unset($chunks[$index]);
282
                continue;
283
            }
284
            
285
            $chunk = preg_replace('~Controller$~', '', $chunk);
286
            if ($chunk) {
287
                $chunk = $this->splitCamelCaseToWords($chunk);
288
                $chunks[$index] = implode(' ', $chunk);
289
            }
290
        }
291
           
292
        return implode('.', $chunks);
293
    } // end generateEndpointKey
294
    
295
    private function getSortedEndpoints($endpoints)
296
    {
297
        ksort($endpoints);
298
299
        $sorted = array();
300
        foreach ($endpoints as $key => $val) {
301
            $this->ins($sorted, explode('.', $key), $val);
302
        }
303
        
304
        return $sorted;
305
    } // end getSortedEndpoints
306
    
307
    private function getUriParams($route)
308
    {
309
        preg_match_all('~{(\w+)}~', $this->getRouteParam($route, 'uri'), $matches);
310
        
311
        return isset($matches[1]) ? $matches[1] : [];
312
    } // end getUriParams
313
    
314
    private function generateHashForUrl($key, $route, $method)
315
    {
316
        $path = preg_replace('~\s+~', '-', $key);
317
        $httpMethod = $this->getRouteParam($route, 'methods.0');
318
        $classMethod = implode('-', $this->splitCamelCaseToWords($method));
319
        
320
        $hash = $path .'::'. $httpMethod .'::'. $classMethod;
321
        
322
        return strtolower($hash);
323
    } // end generateHashForUrl
324
    
325
    private function splitCamelCaseToWords($chunk)
326
    {
327
        $splitCamelCaseRegexp = '/(?#! splitCamelCase Rev:20140412)
328
            # Split camelCase "words". Two global alternatives. Either g1of2:
329
              (?<=[a-z])      # Position is after a lowercase,
330
              (?=[A-Z])       # and before an uppercase letter.
331
            | (?<=[A-Z])      # Or g2of2; Position is after uppercase,
332
              (?=[A-Z][a-z])  # and before upper-then-lower case.
333
            /x';
334
            
335
        return preg_split($splitCamelCaseRegexp, $chunk);
336
    } // end splitCamelCaseToWords
337
    
338
    private function getRouteParam($route, $param)
339
    {
340
        $route = (array) $route;
341
        $prefix = chr(0).'*'.chr(0);
342
        
343
        return $this->arrayGet(
344
            $route, 
345
            $prefix.$param, 
346
            $this->arrayGet($route, $param)
347
        );
348
    } // end getRouteParam
349
    
350
    private function ins(&$ary, $keys, $val) 
351
    {
352
        $keys ? 
353
            $this->ins($ary[array_shift($keys)], $keys, $val) :
354
            $ary = $val;
355
    } // end ins
356
    
357
    private function arrayGet($array, $key, $default = null)
358
    {
359
        if (is_null($key)) {
360
            return $array;
361
        }
362
        
363
        if (isset($array[$key])) {
364
            return $array[$key];
365
        }
366
        
367
        foreach (explode('.', $key) as $segment) {
368
            if (!is_array($array) || ! array_key_exists($segment, $array)) {
369
                return $default;
370
            }
371
            $array = $array[$segment];
372
        }
373
        
374
        return $array;
375
    }
376
    
377
}
378