Completed
Push — master ( 237e1e...8c2ac3 )
by Yaro
01:29
created

ApiDocs   C

Complexity

Total Complexity 60

Size/Duplication

Total Lines 322
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 8
Bugs 0 Features 1
Metric Value
wmc 60
c 8
b 0
f 1
lcom 1
cbo 1
dl 0
loc 322
rs 6.0975

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A show() 0 9 1
A blueprint() 0 10 1
C getEndpoints() 0 36 7
A isExcluded() 0 7 2
A isExcludedRoute() 0 10 3
A isExcludedClass() 0 10 3
A isPrefixedRoute() 0 6 1
A getRoutePrefix() 0 6 2
A getRoutePrefixes() 0 9 2
A isClosureRoute() 0 6 1
C getRouteDocBlock() 0 42 7
B getParamTemplateByType() 0 19 7
A getParamChunksFromLine() 0 10 1
A filterDocBlock() 0 10 2
A generateEndpointKey() 0 20 4
A getSortedEndpoints() 0 11 2
A getUriParams() 0 6 2
A generateHashForUrl() 0 10 1
A splitCamelCaseToWords() 0 12 1
A getRouteParam() 0 11 1
A ins() 0 6 2
B arrayGet() 0 19 6

How to fix   Complexity   

Complex Class

Complex classes like ApiDocs often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ApiDocs, and based on these observations, apply Extract Interface, too.

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
            list($title, $description, $params) = $this->getRouteDocBlock($class, $method);
64
            $key = $this->generateEndpointKey($class);
65
            
66
            $endpoints[$key][] = [
67
                'hash'    => $this->generateHashForUrl($key, $route, $method),
68
                'uri'     => $this->getRouteParam($route, 'uri'),
69
                'name'    => $method,
70
                'methods' => $this->getRouteParam($route, 'methods'),
71
                'docs' => [
72
                    'title'       => $title, 
73
                    'description' => trim($description), 
74
                    'params'      => $params,
75
                    'uri_params'  => $this->getUriParams($route),
76
                ],
77
            ];
78
        }
79
        
80
        return $endpoints;
81
    } // end getEndpoints
82
    
83
    private function isExcluded($route)
84
    {
85
        $uri = $this->getRouteParam($route, 'uri');
86
        $actionController = $this->getRouteParam($route, 'action.controller');
87
        
88
        return $this->isExcludedClass($actionController) || $this->isExcludedRoute($uri);
89
    } // end isExcluded
90
    
91
    private function isExcludedRoute($uri)
92
    {
93
        foreach ($this->config->get('yaro.apidocs.exclude.routes', []) as $pattern) {
94
            if (str_is($pattern, $uri)) {
95
                return true;
96
            }
97
        }
98
        
99
        return false;
100
    } // end isExcludedRoute
101
    
102
    private function isExcludedClass($actionController)
103
    {
104
        foreach ($this->config->get('yaro.apidocs.exclude.classes', []) as $pattern) {
105
            if (str_is($pattern, $actionController)) {
106
                return true;
107
            }
108
        }
109
        
110
        return false;
111
    } // end isExcludedClass
112
    
113
    private function isPrefixedRoute($route, $routePrefix)
114
    {
115
        $regexp = '~^'. preg_quote($routePrefix) .'~';
116
        
117
        return preg_match($regexp, $this->getRouteParam($route, 'uri'));
118
    } // end isPrefixedRoute
119
    
120
    private function getRoutePrefix($routePrefix)
121
    {
122
        $prefixes = $this->getRoutePrefixes();
123
        
124
        return in_array($routePrefix, $prefixes) ? $routePrefix : array_shift($prefixes);
125
    }
126
    
127
    private function getRoutePrefixes()
128
    {
129
        $prefixes = $this->config->get('yaro.apidocs.prefix', 'api');
130
        if (!is_array($prefixes)) {
131
            $prefixes = [$prefixes];
132
        }
133
        
134
        return $prefixes;
135
    }
136
    
137
    private function isClosureRoute($route)
138
    {
139
        $action = $this->getRouteParam($route, 'action.uses');
140
        
141
        return is_object($action);
142
    } // end isClosureRoute
143
    
144
    private function getRouteDocBlock($class, $method)
145
    {
146
        $reflector = new ReflectionClass($class);
147
        
148
        $title = implode(' ', $this->splitCamelCaseToWords($method));
149
        $title = ucfirst(strtolower($title));
150
        $description = '';
151
        $params = [];
152
            
153
        $docs = explode("\n", $reflector->getMethod($method)->getDocComment());
154
        $docs = array_filter($docs);
155
        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...
156
            return [$title, $description, $params];
157
        }
158
        
159
        $docs = $this->filterDocBlock($docs);
160
        
161
        $title = array_shift($docs);
162
        
163
        $checkForLongDescription = true;
164
        foreach ($docs as $line) {
165
            if ($checkForLongDescription && !preg_match('~^@\w+~', $line)) {
166
                $description .= trim($line) .' ';
167
            } elseif (preg_match('~^@\w+~', $line)) {
168
                $checkForLongDescription = false;
169
                if (preg_match('~^@param~', $line)) {
170
                    $paramChunks = $this->getParamChunksFromLine($line);
171
                    
172
                    $paramType = array_shift($paramChunks);
173
                    $paramName = substr(array_shift($paramChunks), 1);
174
                    $params[$paramName] = [
175
                        'type'        => $paramType,
176
                        'name'        => $paramName,
177
                        'description' => implode(' ', $paramChunks),
178
                        'template'    => $this->getParamTemplateByType($paramType),
179
                    ];
180
                }
181
            }
182
        }
183
        
184
        return [$title, $description, $params];
185
    } // end getRouteDocBlock
186
    
187
    private function getParamTemplateByType($paramType)
188
    {
189
        switch ($paramType) {
190
            case 'file':
191
                return 'file';
192
                
193
            case 'bool':
194
            case 'boolean':
195
                return 'boolean';
196
                
197
            case 'int':
198
                return 'integer';
199
                
200
            case 'text':
201
            case 'string':
202
            default:
203
                return 'string';
204
        }
205
    } // end getParamTemplateByType
206
    
207
    private function getParamChunksFromLine($line)
208
    {
209
        $paramChunks = explode(' ', $line);
210
        $paramChunks = array_filter($paramChunks, function($val) {
211
            return $val !== '';
212
        });
213
        unset($paramChunks[0]);
214
        
215
        return $paramChunks;
216
    } // end getParamChunksFromLine
217
    
218
    private function filterDocBlock($docs)
219
    {
220
        foreach ($docs as &$line) {
221
            $line = preg_replace('~\s*\*\s*~', '', $line);
222
            $line = preg_replace('~^/$~', '', $line);
223
        }
224
        $docs = array_values(array_filter($docs));
225
        
226
        return $docs;
227
    } // end filterDocBlock
228
    
229
    private function generateEndpointKey($class)
230
    {
231
        $disabledNamespaces = $this->config->get('yaro.apidocs.disabled_namespaces', []);
232
        
233
        $chunks = explode('\\', $class);
234
        foreach ($chunks as $index => $chunk) {
235
            if (in_array($chunk, $disabledNamespaces)) {
236
                unset($chunks[$index]);
237
                continue;
238
            }
239
            
240
            $chunk = preg_replace('~Controller$~', '', $chunk);
241
            if ($chunk) {
242
                $chunk = $this->splitCamelCaseToWords($chunk);
243
                $chunks[$index] = implode(' ', $chunk);
244
            }
245
        }
246
           
247
        return implode('.', $chunks);
248
    } // end generateEndpointKey
249
    
250
    private function getSortedEndpoints($endpoints)
251
    {
252
        ksort($endpoints);
253
254
        $sorted = array();
255
        foreach ($endpoints as $key => $val) {
256
            $this->ins($sorted, explode('.', $key), $val);
257
        }
258
        
259
        return $sorted;
260
    } // end getSortedEndpoints
261
    
262
    private function getUriParams($route)
263
    {
264
        preg_match_all('~{(\w+)}~', $this->getRouteParam($route, 'uri'), $matches);
265
        
266
        return isset($matches[1]) ? $matches[1] : [];
267
    } // end getUriParams
268
    
269
    private function generateHashForUrl($key, $route, $method)
270
    {
271
        $path = preg_replace('~\s+~', '-', $key);
272
        $httpMethod = $this->getRouteParam($route, 'methods.0');
273
        $classMethod = implode('-', $this->splitCamelCaseToWords($method));
274
        
275
        $hash = $path .'::'. $httpMethod .'::'. $classMethod;
276
        
277
        return strtolower($hash);
278
    } // end generateHashForUrl
279
    
280
    private function splitCamelCaseToWords($chunk)
281
    {
282
        $splitCamelCaseRegexp = '/(?#! splitCamelCase Rev:20140412)
283
            # Split camelCase "words". Two global alternatives. Either g1of2:
284
              (?<=[a-z])      # Position is after a lowercase,
285
              (?=[A-Z])       # and before an uppercase letter.
286
            | (?<=[A-Z])      # Or g2of2; Position is after uppercase,
287
              (?=[A-Z][a-z])  # and before upper-then-lower case.
288
            /x';
289
            
290
        return preg_split($splitCamelCaseRegexp, $chunk);
291
    } // end splitCamelCaseToWords
292
    
293
    private function getRouteParam($route, $param)
294
    {
295
        $route = (array) $route;
296
        $prefix = chr(0).'*'.chr(0);
297
        
298
        return $this->arrayGet(
299
            $route, 
300
            $prefix.$param, 
301
            $this->arrayGet($route, $param)
302
        );
303
    } // end getRouteParam
304
    
305
    private function ins(&$ary, $keys, $val) 
306
    {
307
        $keys ? 
308
            $this->ins($ary[array_shift($keys)], $keys, $val) :
309
            $ary = $val;
310
    } // end ins
311
    
312
    private function arrayGet($array, $key, $default = null)
313
    {
314
        if (is_null($key)) {
315
            return $array;
316
        }
317
        
318
        if (isset($array[$key])) {
319
            return $array[$key];
320
        }
321
        
322
        foreach (explode('.', $key) as $segment) {
323
            if (!is_array($array) || ! array_key_exists($segment, $array)) {
324
                return $default;
325
            }
326
            $array = $array[$segment];
327
        }
328
        
329
        return $array;
330
    }
331
    
332
}
333