Completed
Push — master ( 0d17ec...34df22 )
by Yaro
02:05
created

ApiDocs   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 228
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 5
Bugs 0 Features 1
Metric Value
wmc 34
c 5
b 0
f 1
lcom 1
cbo 1
dl 0
loc 228
rs 9.2

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A show() 0 7 1
A blueprint() 0 7 1
B getEndpoints() 0 36 6
A isPrefixedRoute() 0 7 1
A isClosureRoute() 0 6 1
C getRouteDocBlock() 0 41 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
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)) {
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 isPrefixedRoute($route)
76
    {
77
        $prefix = $this->config->get('yaro.apidocs.prefix', 'api');
78
        $regexp = '~^'. preg_quote($prefix) .'~';
79
        
80
        return preg_match($regexp, $this->getRouteParam($route, 'uri'));
81
    } // end isPrefixedRoute
82
    
83
    private function isClosureRoute($route)
84
    {
85
        $action = $this->getRouteParam($route, 'action.uses');
86
        
87
        return is_object($action);
88
    } // end isClosureRoute
89
    
90
    private function getRouteDocBlock($class, $method)
91
    {
92
        $reflector = new ReflectionClass($class);
93
        
94
        $title = implode(' ', $this->splitCamelCaseToWords($method));
95
        $title = ucfirst(strtolower($title));
96
        $description = '';
97
        $params = [];
98
            
99
        $docs = explode("\n", $reflector->getMethod($method)->getDocComment());
100
        $docs = array_filter($docs);
101
        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...
102
            return [$title, $description, $params];
103
        }
104
        
105
        $docs = $this->filterDocBlock($docs);
106
        
107
        $title = array_shift($docs);
108
        
109
        $checkForLongDescription = true;
110
        foreach ($docs as $line) {
111
            if ($checkForLongDescription && !preg_match('~^@\w+~', $line)) {
112
                $description .= trim($line) .' ';
113
            } elseif (preg_match('~^@\w+~', $line)) {
114
                $checkForLongDescription = false;
115
                if (preg_match('~^@param~', $line)) {
116
                    $paramChunks = $this->getParamChunksFromLine($line);
117
                    
118
                    $paramType = array_shift($paramChunks);
119
                    $paramName = substr(array_shift($paramChunks), 1);
120
                    $params[$paramName] = [
121
                        'type'        => $paramType,
122
                        'name'        => $paramName,
123
                        'description' => implode(' ', $paramChunks),
124
                    ];
125
                }
126
            }
127
        }
128
        
129
        return [$title, $description, $params];
130
    } // end getRouteDocBlock
131
    
132
    private function getParamChunksFromLine($line)
133
    {
134
        $paramChunks = explode(' ', $line);
135
        $paramChunks = array_filter($paramChunks, function($val) {
136
            return $val !== '';
137
        });
138
        unset($paramChunks[0]);
139
        
140
        return $paramChunks;
141
    } // end getParamChunksFromLine
142
    
143
    private function filterDocBlock($docs)
144
    {
145
        foreach ($docs as &$line) {
146
            $line = preg_replace('~\s*\*\s*~', '', $line);
147
            $line = preg_replace('~^/$~', '', $line);
148
        }
149
        $docs = array_values(array_filter($docs));
150
        
151
        return $docs;
152
    } // end filterDocBlock
153
    
154
    private function generateEndpointKey($class)
155
    {
156
        $disabledNamespaces = $this->config->get('yaro.apidocs.disabled_namespaces', []);
157
        
158
        $chunks = explode('\\', $class);
159
        foreach ($chunks as $index => $chunk) {
160
            if (in_array($chunk, $disabledNamespaces)) {
161
                unset($chunks[$index]);
162
                continue;
163
            }
164
            
165
            $chunk = preg_replace('~Controller$~', '', $chunk);
166
            if ($chunk) {
167
                $chunk = $this->splitCamelCaseToWords($chunk);
168
                $chunks[$index] = implode(' ', $chunk);
169
            }
170
        }
171
           
172
        return implode('.', $chunks);
173
    } // end generateEndpointKey
174
    
175
    private function getSortedEndpoints($endpoints)
176
    {
177
        ksort($endpoints);
178
179
        $sorted = array();
180
        foreach($endpoints as $key => $val) {
181
            $this->ins($sorted, explode('.', $key), $val);
182
        }
183
        
184
        return $sorted;
185
    } // end getSortedEndpoints
186
    
187
    private function getUriParams($route)
188
    {
189
        preg_match_all('~{(\w+)}~', $this->getRouteParam($route, 'uri'), $matches);
190
        
191
        return isset($matches[1]) ? $matches[1] : [];
192
    } // end getUriParams
193
    
194
    private function generateHashForUrl($key, $route, $method)
195
    {
196
        $path = preg_replace('~\s+~', '-', $key);
197
        $httpMethod = $this->getRouteParam($route, 'methods.0');
198
        $classMethod = implode('-', $this->splitCamelCaseToWords($method));
199
        
200
        $hash = $path .'::'. $httpMethod .'::'. $classMethod;
201
        
202
        return strtolower($hash);
203
    } // end generateHashForUrl
204
    
205
    private function splitCamelCaseToWords($chunk)
206
    {
207
        $splitCamelCaseRegexp = '/(?#! splitCamelCase Rev:20140412)
208
            # Split camelCase "words". Two global alternatives. Either g1of2:
209
              (?<=[a-z])      # Position is after a lowercase,
210
              (?=[A-Z])       # and before an uppercase letter.
211
            | (?<=[A-Z])      # Or g2of2; Position is after uppercase,
212
              (?=[A-Z][a-z])  # and before upper-then-lower case.
213
            /x';
214
            
215
        return preg_split($splitCamelCaseRegexp, $chunk);
216
    } // end splitCamelCaseToWords
217
    
218
    private function getRouteParam($route, $param)
219
    {
220
        $route = (array) $route;
221
        $prefix = chr(0).'*'.chr(0);
222
        
223
        return array_get(
224
            $route, 
225
            $prefix.$param, 
226
            array_get($route, $param)
227
        );
228
    } // end getRouteParam
229
    
230
    private function ins(&$ary, $keys, $val) 
231
    {
232
        $keys ? 
233
            $this->ins($ary[array_shift($keys)], $keys, $val) :
234
            $ary = $val;
235
    } // end ins
236
    
237
}
238