Issues (6)

src/Router.php (1 issue)

Severity
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Micro
7
 *
8
 * @author      Raffael Sahli <[email protected]>
9
 * @copyright   Copryright (c) 2015-2018 gyselroth GmbH (https://gyselroth.com)
10
 * @license     MIT https://opensource.org/licenses/MIT
11
 */
12
13
namespace Micro\Http;
14
15
use Micro\Http\Router\Route;
16
use Psr\Container\ContainerInterface;
17
use Psr\Log\LoggerInterface;
18
use ReflectionException;
19
use ReflectionMethod;
20
21
class Router
22
{
23
    /**
24
     * Requested route.
25
     *
26
     * @var string
27
     */
28
    protected $path;
29
30
    /**
31
     * HTTP verb.
32
     *
33
     * @var string
34
     */
35
    protected $verb;
36
37
    /**
38
     * Installed routes.
39
     *
40
     * @var array
41
     */
42
    protected $routes = [];
43
44
    /**
45
     * Logger.
46
     *
47
     * @var LoggerInterface
48
     */
49
    protected $logger;
50
51
    /**
52
     * DI container.
53
     *
54
     * @var ContainerInterface
55
     */
56
    protected $container;
57
58
    /**
59
     * Content type.
60
     *
61
     * @var string
62
     */
63
    protected $content_type;
64
65
    /**
66
     * Init router.
67
     *
68
     * @param LoggerInterface    $logger
69
     * @param ContainerInterface $container
70
     * @param array              $request
71
     */
72
    public function __construct(LoggerInterface $logger, ?array $request = null, ?ContainerInterface $container = null)
73
    {
74
        $this->logger = $logger;
75
        $this->container = $container;
76
77
        if (null === $request) {
78
            $request = $_SERVER;
79
        }
80
81
        if (isset($request['CONTENT_TYPE'])) {
82
            $this->setContentType($request['CONTENT_TYPE']);
83
        }
84
85
        if (isset($request['PATH_INFO'])) {
86
            $this->setPath($request['PATH_INFO']);
87
        }
88
89
        if (isset($request['REQUEST_METHOD'])) {
90
            $this->setVerb($request['REQUEST_METHOD']);
91
        }
92
    }
93
94
    /**
95
     * Add route to the beginning of the routing table.
96
     *
97
     * @param Route $route
98
     *
99
     * @return Router
100
     */
101
    public function prependRoute(Route $route): self
102
    {
103
        array_unshift($this->routes, $route);
104
        $route->setRouter($this);
105
106
        return $this;
107
    }
108
109
    /**
110
     * Add route to the end of the routing table.
111
     *
112
     * @param Route $route
113
     *
114
     * @return Router
115
     */
116
    public function appendRoute(Route $route): self
117
    {
118
        $this->routes[] = $route;
119
        $route->setRouter($this);
120
121
        return $this;
122
    }
123
124
    /**
125
     * Clear routing table.
126
     *
127
     * @return Router
128
     */
129
    public function clearRoutingTable(): self
130
    {
131
        $this->routes = [];
132
133
        return $this;
134
    }
135
136
    /**
137
     * Get active routes.
138
     *
139
     * @return array
140
     */
141
    public function getRoutes(): array
142
    {
143
        return $this->routes;
144
    }
145
146
    /**
147
     * Set Content type.
148
     *
149
     * @param string $type
150
     *
151
     * @return Router
152
     */
153
    public function setContentType(string $type): self
154
    {
155
        $parts = explode(';', $type);
156
        $this->content_type = $parts[0];
157
158
        return $this;
159
    }
160
161
    /**
162
     * Get content type.
163
     *
164
     * @return string
165
     */
166
    public function getContentType(): string
167
    {
168
        return $this->content_type;
169
    }
170
171
    /**
172
     * Set HTTP verb.
173
     *
174
     * @param string $verb
175
     *
176
     * @return Router
177
     */
178
    public function setVerb(string $verb): self
179
    {
180
        $this->verb = strtolower($verb);
181
182
        return $this;
183
    }
184
185
    /**
186
     * Get http verb.
187
     *
188
     * @return string
189
     */
190
    public function getVerb(): string
191
    {
192
        return $this->verb;
193
    }
194
195
    /**
196
     * Set routing path.
197
     *
198
     * @param string $path
199
     *
200
     * @return Router
201
     */
202
    public function setPath(string $path): self
203
    {
204
        $path = rtrim(trim($path), '/');
205
        $this->path = (string) $path;
206
207
        return $this;
208
    }
209
210
    /**
211
     * Get path.
212
     *
213
     * @return string
214
     */
215
    public function getPath(): string
216
    {
217
        return $this->path;
218
    }
219
220
    /**
221
     * Execute router.
222
     *
223
     * @return bool
224
     */
225
    public function run(): bool
226
    {
227
        $this->logger->info('execute requested route ['.$this->path.']', [
228
            'category' => get_class($this),
229
        ]);
230
231
        $response = null;
232
233
        try {
234
            $match = false;
235
            foreach ($this->routes as $key => $route) {
236
                if ($route->match()) {
237
                    $callable = $route->getCallable($this->container);
238
239
                    if (is_callable($callable)) {
240
                        $match = true;
241
                        $this->logger->info('found matching route ['.$route->getPath().'], execute ['.$route->getClass().'::'.$callable[1].']', [
242
                            'category' => get_class($this),
243
                        ]);
244
245
                        $params = $this->getParams($route->getClass(), $callable[1], $route->getParams());
246
                        $response = call_user_func_array($callable, $params);
247
248
                        if (!$route->continueAfterMatch()) {
249
                            break;
250
                        }
251
                    } else {
252
                        $this->logger->debug('found matching route ['.$route->getPath().'], but callable ['.$route->getClass().'::'.$callable[1].'] was not found', [
253
                            'category' => get_class($this),
254
                        ]);
255
                    }
256
                } else {
257
                    $this->logger->debug('requested path ['.$this->path.'] does not match route ['.$route->getPath().']', [
258
                        'category' => get_class($this),
259
                    ]);
260
                }
261
            }
262
263
            if (false === $match) {
264
                throw new Exception\NoRouteMatch($this->verb.' '.$this->path.' could not be routed, no matching routes found');
265
            }
266
267
            if ($response instanceof Response) {
268
                $this->logger->info('send http response ['.$response->getCode().']', [
269
                        'category' => get_class($this),
270
                    ]);
271
272
                $response->send();
273
            } else {
274
                $this->logger->debug('callback did not return a response, route exectuted successfully', [
275
                    'category' => get_class($this),
276
                ]);
277
            }
278
279
            return true;
280
        } catch (\Exception $e) {
281
            return $this->sendException($e);
282
        }
283
    }
284
285
    /**
286
     * Sends a exception response to the client.
287
     *
288
     * @param \Exception $exception
289
     *
290
     * @return bool
291
     */
292
    public function sendException(\Exception $exception): bool
293
    {
294
        $message = $exception->getMessage();
295
        $class = get_class($exception);
296
297
        $msg = [
298
            'error' => $class,
299
            'message' => $message,
300
            'code' => $exception->getCode(),
301
        ];
302
303
        if ($exception instanceof ExceptionInterface) {
304
            $http_code = $exception->getStatusCode();
305
        } else {
306
            $http_code = 500;
307
        }
308
309
        $this->logger->error('uncaught exception '.$message.']', [
310
            'category' => get_class($this),
311
            'exception' => $exception,
312
        ]);
313
314
        (new Response())
315
            ->setCode($http_code)
316
            ->setBody($msg)
317
            ->send();
318
319
        return true;
320
    }
321
322
    /**
323
     * Build method name.
324
     *
325
     * @param string $name
326
     *
327
     * @return string
328
     */
329
    protected function _buildMethodName(string $name): string
330
    {
331
        $result = $this->verb;
332
        $split = explode('-', $name);
333
        foreach ($split as $part) {
334
            $result .= ucfirst($part);
335
        }
336
337
        return $result;
338
    }
339
340
    /**
341
     * Decode request.
342
     *
343
     * @param array $parsed_params
344
     *
345
     * @return array
346
     */
347
    protected function decodeRequest(array $parsed_params): array
348
    {
349
        if ('application/x-www-form-urlencoded' === $this->content_type) {
350
            $body = file_get_contents('php://input');
351
            parse_str($body, $decode);
352
353
            return array_merge($decode, $_REQUEST, $parsed_params);
354
        }
355
        if ('application/json' === $this->content_type || 'application/json; charset=utf-8' === $this->content_type) {
356
            $body = file_get_contents('php://input');
357
            $json_params = [];
358
359
            if (!empty($body)) {
360
                $json_params = json_decode($body, true);
361
            } else {
362
                $parts = explode('&', $_SERVER['QUERY_STRING']);
363
                if (!empty($parts)) {
364
                    $json_params = json_decode(urldecode($parts[0]), true);
365
                }
366
            }
367
368
            if (JSON_ERROR_NONE !== json_last_error()) {
369
                throw new Exception\InvalidJson('invalid json input given');
370
            }
371
372
            return array_merge($json_params, $_REQUEST, $parsed_params);
373
        }
374
375
        return array_merge($parsed_params, $_REQUEST);
376
    }
377
378
    /**
379
     * Check if method got params and combine these with
380
     * $_REQUEST.
381
     *
382
     * @param string $class
383
     * @param string $method
384
     * @param array  $parsed_params
385
     *
386
     * @return callable
387
     */
388
    protected function getParams(string $class, string $method, array $parsed_params): array
389
    {
390
        try {
391
            $return = [];
392
            $meta = new ReflectionMethod($class, $method);
393
            $params = $meta->getParameters();
394
            $json_params = [];
395
            $request_params = $this->decodeRequest($parsed_params);
396
397
            foreach ($params as $param) {
398
                $type = (string) $param->getType();
399
                $optional = $param->isOptional();
400
401
                if (isset($request_params[$param->name]) && '' !== $request_params[$param->name]) {
402
                    $param_value = $request_params[$param->name];
403
                } elseif (isset($json_params[$param->name])) {
404
                    $param_value = $json_params[$param->name];
405
                } elseif (true === $optional) {
406
                    $return[$param->name] = $param->getDefaultValue();
407
408
                    continue;
409
                } else {
410
                    $param_value = null;
411
                }
412
413
                if (null !== $param->getClass() && null === $param_value && null !== $this->container) {
414
                    $return[$param->name] = $this->container->get($type);
415
416
                    continue;
417
                }
418
419
                if (null === $param_value && false === $optional) {
420
                    throw new Exception\MissingInputArgument('misssing required input parameter '.$param->name);
421
                }
422
423
                $return[$param->name] = $this->convertParam($type, $param_value);
424
            }
425
426
            return $return;
427
        } catch (ReflectionException $e) {
428
            throw new Exception\MissingInputArgument('misssing or invalid required request parameter');
429
        }
430
    }
431
432
    /**
433
     * Convert param.
434
     *
435
     * @param string $type
436
     * @param mixed  $value
437
     *
438
     * @return mixed
439
     */
440
    protected function convertParam(string $type, $value)
441
    {
442
        switch ($type) {
443
            case 'bool':
444
                if ('false' === $value) {
445
                    return false;
446
                }
447
448
                    return (bool) $value;
449
450
            break;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
451
            case 'int':
452
                return (int) $value;
453
454
            break;
455
            case 'float':
456
                return (float) $value;
457
458
            break;
459
            case 'array':
460
                return (array) $value;
461
462
            break;
463
            default:
464
                if (class_exists($type) && null !== $value) {
465
                    return new $type($value);
466
                }
467
468
                    return $value;
469
470
            break;
471
        }
472
    }
473
}
474