ApiTester::getRoutes()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 15
nc 2
nop 0
dl 0
loc 20
rs 9.7666
c 0
b 0
f 0
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: sheldon
5
 * Date: 18-4-19
6
 * Time: 上午9:41.
7
 */
8
9
namespace Yeelight\Models\Tools\ApiTester;
10
11
use Illuminate\Foundation\Application;
12
use Illuminate\Http\Request;
13
use Illuminate\Http\UploadedFile;
14
use Illuminate\Routing\Route;
15
use Illuminate\Support\Arr;
16
use Illuminate\Support\Str;
17
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
18
use Symfony\Component\HttpFoundation\Response;
19
20
/**
21
 * Class ApiTester
22
 *
23
 * @category Yeelight
24
 *
25
 * @package Yeelight\Models\Tools\ApiTester
26
 *
27
 * @author Sheldon Lee <[email protected]>
28
 *
29
 * @license https://opensource.org/licenses/MIT MIT
30
 *
31
 * @link https://www.yeelight.com
32
 */
33
class ApiTester
34
{
35
    /**
36
     * The Illuminate application instance.
37
     *
38
     * @var \Illuminate\Foundation\Application
39
     */
40
    protected $app;
41
42
    /**
43
     * $methodColors
44
     *
45
     * @var array
46
     */
47
    public static $methodColors = [
48
        'GET'    => 'green',
49
        'HEAD'   => 'gray',
50
        'POST'   => 'blue',
51
        'PUT'    => 'yellow',
52
        'DELETE' => 'red',
53
        'PATCH'  => 'aqua',
54
    ];
55
56
    /**
57
     * ApiTester constructor.
58
     *
59
     * @param \Illuminate\Foundation\Application|null $app Application
60
     */
61
    public function __construct(Application $app = null)
62
    {
63
        $this->app = $app ?: app();
64
    }
65
66
    /**
67
     * Call
68
     *
69
     * @param string $method
70
     * @param string $uri
71
     * @param array  $parameters
72
     * @param null   $user
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $user is correct as it would always require null to be passed?
Loading history...
73
     *
74
     * @return Response
75
     *
76
     * @internal param string $userId
77
     */
78
    public function call($method, $uri, $parameters = [], $user = null)
79
    {
80
        //ApiLogger::log(...func_get_args());
81
        if ($user) {
82
            $this->loginUsing($user);
83
        }
84
        $kernel = $this->app->make('Illuminate\Contracts\Http\Kernel');
85
        $uri = $this->prepareUrlForRequest($uri);
86
        $files = [];
87
        foreach ($parameters as $key => $val) {
88
            if ($val instanceof UploadedFile) {
89
                $files[$key] = $val;
90
                unset($parameters[$key]);
91
            }
92
        }
93
        $symfonyRequest = SymfonyRequest::create(
94
            $uri, $method, $parameters,
95
            [], $files, ['HTTP_ACCEPT' => 'application/json']
96
        );
97
        $request = Request::createFromBase($symfonyRequest);
98
99
        try {
100
            $response = $kernel->handle($request);
101
        } catch (\Exception $e) {
102
            $response = app('Illuminate\Contracts\Debug\ExceptionHandler')->render($request, $e);
103
        }
104
        $kernel->terminate($request, $response);
105
106
        return $response;
107
    }
108
109
    /**
110
     * Login a user by giving userid.
111
     *
112
     * @param $userId
113
     */
114
    protected function loginUsing($userId)
115
    {
116
        $guard = config('yeelight.backend.tools.api-tester.guard', 'api');
117
        if ($method = config('yeelight.backend.tools.api-tester.user_retriever')) {
118
            $user = call_user_func($method, $userId);
119
        } else {
120
            $user = app('auth')->guard($guard)->getProvider()->retrieveById($userId);
121
        }
122
        $this->app['auth']->guard($guard)->setUser($user);
123
    }
124
125
    /**
126
     * @param Response $response Response
127
     *
128
     * @return array
129
     */
130
    public function parseResponse(Response $response)
131
    {
132
        $content = $response->getContent();
133
        $jsoned = json_decode($content);
134
        if (json_last_error() == JSON_ERROR_NONE) {
135
            $content = json_encode($jsoned, JSON_PRETTY_PRINT);
136
        }
137
        $lang = 'json';
138
        $contentType = $response->headers->get('content-type');
139
        if (Str::contains($contentType, 'html')) {
140
            $lang = 'html';
141
        }
142
143
        return [
144
            'headers'    => json_encode($response->headers->all(), JSON_PRETTY_PRINT),
145
            'cookies'    => json_encode($response->headers->getCookies(), JSON_PRETTY_PRINT),
146
            'content'    => $content,
147
            'language'   => $lang,
148
            'status'     => [
149
                'code'  => $response->getStatusCode(),
150
                'text'  => $this->getStatusText($response),
151
            ],
152
        ];
153
    }
154
155
    /**
156
     * @param Response $response
157
     *
158
     * @return string
159
     */
160
    protected function getStatusText(Response $response)
161
    {
162
        $statusText = new \ReflectionProperty($response, 'statusText');
163
        $statusText->setAccessible(true);
164
165
        return $statusText->getValue($response);
166
    }
167
168
    /**
169
     * Filter the given array of files, removing any empty values.
170
     *
171
     * @param array $files
172
     *
173
     * @return mixed
174
     */
175
    protected function filterFiles($files)
176
    {
177
        foreach ($files as $key => $file) {
178
            if ($file instanceof UploadedFile) {
179
                continue;
180
            }
181
            if (is_array($file)) {
182
                if (!isset($file['name'])) {
183
                    $files[$key] = $this->filterFiles($files[$key]);
184
                } elseif (isset($files[$key]['error']) && $files[$key]['error'] !== 0) {
185
                    unset($files[$key]);
186
                }
187
                continue;
188
            }
189
            unset($files[$key]);
190
        }
191
192
        return $files;
193
    }
194
195
    /**
196
     * Turn the given URI into a fully qualified URL.
197
     *
198
     * @param string $uri
199
     *
200
     * @return string
201
     */
202
    protected function prepareUrlForRequest($uri)
203
    {
204
        if (Str::startsWith($uri, '/')) {
205
            $uri = substr($uri, 1);
206
        }
207
        if (!Str::startsWith($uri, 'http')) {
208
            $uri = config('app.url').'/'.$uri;
209
        }
210
211
        return trim($uri, '/');
212
    }
213
214
    /**
215
     * Get all api routes.
216
     *
217
     * @return array
218
     */
219
    public function getRoutes()
220
    {
221
        $routes = app('router')->getRoutes();
222
        $prefix = config('yeelight.backend.tools.api-tester.prefix');
223
        $routes = collect($routes)->filter(function ($route) use ($prefix) {
0 ignored issues
show
Unused Code introduced by
The import $prefix is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
224
            return Str::startsWith($route->uri, config('yeelight.backend.tools.api-tester.prefix'));
225
        })->map(function ($route) {
226
            return $this->getRouteInformation($route);
227
        })->all();
228
        if ($sort = request('_sort')) {
229
            $routes = $this->sortRoutes($sort, $routes);
0 ignored issues
show
Bug introduced by
It seems like $sort can also be of type array; however, parameter $sort of Yeelight\Models\Tools\Ap...ApiTester::sortRoutes() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

229
            $routes = $this->sortRoutes(/** @scrutinizer ignore-type */ $sort, $routes);
Loading history...
230
        }
231
        $routes = collect($routes)->filter()->map(function ($route) {
232
            $route['parameters'] = json_encode($this->getRouteParameters($route['action']));
233
            unset($route['middleware'], $route['host'], $route['name'], $route['action']);
234
235
            return $route;
236
        })->toArray();
237
238
        return array_filter($routes);
239
    }
240
241
    /**
242
     * Get parameters info of route.
243
     *
244
     * @param $action
245
     *
246
     * @return array
247
     */
248
    protected function getRouteParameters($action)
249
    {
250
        if (is_callable($action) || $action === 'Closure') {
251
            return [];
252
        }
253
        if (is_string($action) && !Str::contains($action, '@')) {
254
            list($class, $method) = static::makeInvokable($action);
255
        } else {
256
            list($class, $method) = explode('@', $action);
257
        }
258
        $classReflector = new \ReflectionClass($class);
259
        $comment = $classReflector->getMethod($method)->getDocComment();
260
        if ($comment) {
261
            $parameters = [];
262
            preg_match_all('/\@SWG\\\Parameter\(\n(.*?)\)\n/s', $comment, $matches);
263
            foreach (array_get($matches, 1, []) as $item) {
264
                preg_match_all('/(\w+)=[\'"]?([^\r\n"]+)[\'"]?,?\n/s', $item, $match);
265
                if (count($match) == 3) {
266
                    $match[2] = array_map(function ($val) {
267
                        return trim($val, ',');
268
                    }, $match[2]);
269
                    $parameters[] = array_combine($match[1], $match[2]);
270
                }
271
            }
272
273
            return $parameters;
274
        }
275
276
        return [];
277
    }
278
279
    /**
280
     * @param $action
281
     *
282
     * @return array
283
     */
284
    protected static function makeInvokable($action)
285
    {
286
        if (!method_exists($action, '__invoke')) {
287
            throw new \UnexpectedValueException("Invalid route action: [{$action}].");
288
        }
289
290
        return [$action, '__invoke'];
291
    }
292
293
    /**
294
     * Get the route information for a given route.
295
     *
296
     * @param \Illuminate\Routing\Route $route
297
     *
298
     * @return array
299
     */
300
    protected function getRouteInformation(Route $route)
301
    {
302
        return [
303
            'host'       => $route->domain(),
304
            'method'     => $route->methods()[0],
305
            'uri'        => $route->uri(),
306
            'name'       => $route->getName(),
307
            'action'     => $route->getActionName(),
308
            'middleware' => $this->getRouteMiddleware($route),
309
        ];
310
    }
311
312
    /**
313
     * Sort the routes by a given element.
314
     *
315
     * @param string $sort
316
     * @param array  $routes
317
     *
318
     * @return array
319
     */
320
    protected function sortRoutes($sort, $routes)
321
    {
322
        return Arr::sort($routes, function ($route) use ($sort) {
323
            return $route[$sort];
324
        });
325
    }
326
327
    /**
328
     * Get before filters.
329
     *
330
     * @param \Illuminate\Routing\Route $route
331
     *
332
     * @return string
333
     */
334
    protected function getRouteMiddleware($route)
335
    {
336
        return collect($route->gatherMiddleware())->map(function ($middleware) {
337
            return $middleware instanceof \Closure ? 'Closure' : $middleware;
338
        });
339
    }
340
}
341