Completed
Push — master ( dc98f7...630443 )
by Yaro
01:53
created

ApiDocs::isExcluded()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
eloc 4
nc 2
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 Yaro\ApiDocs\Blueprint;
9
10
class ApiDocs
11
{
12
    
13
    private $router;
14
    private $config;
15
    
16
    public function __construct(Router $router, Config $config)
17
    {
18
        $this->router = $router;
19
        $this->config = $config;
20
    } // end __construct
21
22
    public function show()
23
    {
24
        $endpoints = $this->getEndpoints();
25
        $endpoints = $this->getSortedEndpoints($endpoints);
26
        
27
        return view('apidocs::docs', compact('endpoints'));
28
    } // end show
29
30
    public function blueprint()
31
    {
32
        $blueprint = app()->make(Blueprint::class);
33
        $blueprint->setEndpoints($this->getEndpoints());
34
        
35
        return $blueprint;
36
    } // end blueprint
37
    
38
    private function getEndpoints()
39
    {
40
        $endpoints = [];
41
        
42
        foreach ($this->router->getRoutes() as $route) {
43
            if (!$this->isPrefixedRoute($route) || $this->isClosureRoute($route) || $this->isExcluded($route)) {
44
                continue;
45
            }
46
            
47
            $actionController = explode("@", $this->getRouteParam($route, 'action.controller'));
48
            $class  = $actionController[0];
49
            $method = $actionController[1];
50
            
51
            if (!class_exists($class) || !method_exists($class, $method)) {
52
                continue;
53
            }
54
            
55
            list($title, $description, $params) = $this->getRouteDocBlock($class, $method);
56
            $key = $this->generateEndpointKey($class);
57
            
58
            $endpoints[$key][] = [
59
                'hash'    => $this->generateHashForUrl($key, $route, $method),
60
                'uri'     => $this->getRouteParam($route, 'uri'),
61
                'name'    => $method,
62
                'methods' => $this->getRouteParam($route, 'methods'),
63
                'docs' => [
64
                    'title'       => $title, 
65
                    'description' => trim($description), 
66
                    'params'      => $params,
67
                    'uri_params'  => $this->getUriParams($route),
68
                ],
69
            ];
70
        }
71
        
72
        return $endpoints;
73
    } // end getEndpoints
74
    
75
    private function isExcluded($route)
76
    {
77
        $uri = $this->getRouteParam($route, 'uri');
78
        $actionController = $this->getRouteParam($route, 'action.controller');
79
        
80
        return $this->isExcludedClass($actionController) || $this->isExcludedRoute($uri);
81
    } // end isExcluded
82
    
83
    private function isExcludedRoute($uri)
84
    {
85
        foreach ($this->config->get('yaro.apidocs.exclude.routes', []) as $pattern) {
86
            if (str_is($pattern, $uri)) {
87
                return true;
88
            }
89
        }
90
        
91
        return false;
92
    } // end isExcludedRoute
93
    
94
    private function isExcludedClass($actionController)
95
    {
96
        foreach ($this->config->get('yaro.apidocs.exclude.classes', []) as $pattern) {
97
            if (str_is($pattern, $actionController)) {
98
                return true;
99
            }
100
        }
101
        
102
        return false;
103
    } // end isExcludedClass
104
    
105
    private function isPrefixedRoute($route)
106
    {
107
        $prefix = $this->config->get('yaro.apidocs.prefix', 'api');
108
        $regexp = '~^'. preg_quote($prefix) .'~';
109
        
110
        return preg_match($regexp, $this->getRouteParam($route, 'uri'));
111
    } // end isPrefixedRoute
112
    
113
    private function isClosureRoute($route)
114
    {
115
        $action = $this->getRouteParam($route, 'action.uses');
116
        
117
        return is_object($action);
118
    } // end isClosureRoute
119
    
120
    private function getRouteDocBlock($class, $method)
121
    {
122
        $reflector = new ReflectionClass($class);
123
        
124
        $title = implode(' ', $this->splitCamelCaseToWords($method));
125
        $title = ucfirst(strtolower($title));
126
        $description = '';
127
        $params = [];
128
            
129
        $docs = explode("\n", $reflector->getMethod($method)->getDocComment());
130
        $docs = array_filter($docs);
131
        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...
132
            return [$title, $description, $params];
133
        }
134
        
135
        $docs = $this->filterDocBlock($docs);
136
        
137
        $title = array_shift($docs);
138
        
139
        $checkForLongDescription = true;
140
        foreach ($docs as $line) {
141
            if ($checkForLongDescription && !preg_match('~^@\w+~', $line)) {
142
                $description .= trim($line) .' ';
143
            } elseif (preg_match('~^@\w+~', $line)) {
144
                $checkForLongDescription = false;
145
                if (preg_match('~^@param~', $line)) {
146
                    $paramChunks = $this->getParamChunksFromLine($line);
147
                    
148
                    $paramType = array_shift($paramChunks);
149
                    $paramName = substr(array_shift($paramChunks), 1);
150
                    $params[$paramName] = [
151
                        'type'        => $paramType,
152
                        'name'        => $paramName,
153
                        'description' => implode(' ', $paramChunks),
154
                    ];
155
                }
156
            }
157
        }
158
        
159
        return [$title, $description, $params];
160
    } // end getRouteDocBlock
161
    
162
    private function getParamChunksFromLine($line)
163
    {
164
        $paramChunks = explode(' ', $line);
165
        $paramChunks = array_filter($paramChunks, function($val) {
166
            return $val !== '';
167
        });
168
        unset($paramChunks[0]);
169
        
170
        return $paramChunks;
171
    } // end getParamChunksFromLine
172
    
173
    private function filterDocBlock($docs)
174
    {
175
        foreach ($docs as &$line) {
176
            $line = preg_replace('~\s*\*\s*~', '', $line);
177
            $line = preg_replace('~^/$~', '', $line);
178
        }
179
        $docs = array_values(array_filter($docs));
180
        
181
        return $docs;
182
    } // end filterDocBlock
183
    
184
    private function generateEndpointKey($class)
185
    {
186
        $disabledNamespaces = $this->config->get('yaro.apidocs.disabled_namespaces', []);
187
        
188
        $chunks = explode('\\', $class);
189
        foreach ($chunks as $index => $chunk) {
190
            if (in_array($chunk, $disabledNamespaces)) {
191
                unset($chunks[$index]);
192
                continue;
193
            }
194
            
195
            $chunk = preg_replace('~Controller$~', '', $chunk);
196
            if ($chunk) {
197
                $chunk = $this->splitCamelCaseToWords($chunk);
198
                $chunks[$index] = implode(' ', $chunk);
199
            }
200
        }
201
           
202
        return implode('.', $chunks);
203
    } // end generateEndpointKey
204
    
205
    private function getSortedEndpoints($endpoints)
206
    {
207
        ksort($endpoints);
208
209
        $sorted = array();
210
        foreach ($endpoints as $key => $val) {
211
            $this->ins($sorted, explode('.', $key), $val);
212
        }
213
        
214
        return $sorted;
215
    } // end getSortedEndpoints
216
    
217
    private function getUriParams($route)
218
    {
219
        preg_match_all('~{(\w+)}~', $this->getRouteParam($route, 'uri'), $matches);
220
        
221
        return isset($matches[1]) ? $matches[1] : [];
222
    } // end getUriParams
223
    
224
    private function generateHashForUrl($key, $route, $method)
225
    {
226
        $path = preg_replace('~\s+~', '-', $key);
227
        $httpMethod = $this->getRouteParam($route, 'methods.0');
228
        $classMethod = implode('-', $this->splitCamelCaseToWords($method));
229
        
230
        $hash = $path .'::'. $httpMethod .'::'. $classMethod;
231
        
232
        return strtolower($hash);
233
    } // end generateHashForUrl
234
    
235
    private function splitCamelCaseToWords($chunk)
236
    {
237
        $splitCamelCaseRegexp = '/(?#! splitCamelCase Rev:20140412)
238
            # Split camelCase "words". Two global alternatives. Either g1of2:
239
              (?<=[a-z])      # Position is after a lowercase,
240
              (?=[A-Z])       # and before an uppercase letter.
241
            | (?<=[A-Z])      # Or g2of2; Position is after uppercase,
242
              (?=[A-Z][a-z])  # and before upper-then-lower case.
243
            /x';
244
            
245
        return preg_split($splitCamelCaseRegexp, $chunk);
246
    } // end splitCamelCaseToWords
247
    
248
    private function getRouteParam($route, $param)
249
    {
250
        $route = (array) $route;
251
        $prefix = chr(0).'*'.chr(0);
252
        
253
        return array_get(
254
            $route, 
255
            $prefix.$param, 
256
            array_get($route, $param)
257
        );
258
    } // end getRouteParam
259
    
260
    private function ins(&$ary, $keys, $val) 
261
    {
262
        $keys ? 
263
            $this->ins($ary[array_shift($keys)], $keys, $val) :
264
            $ary = $val;
265
    } // end ins
266
    
267
}
268