Completed
Push — master ( 463490...6626e8 )
by Yaro
01:35
created

ApiDocs::getEndpoints()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 36
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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