Completed
Push — master ( 530380...a2b21a )
by Yaro
01:29
created

ApiDocs::generateHashForUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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