Passed
Push — master ( 23e1ba...84f37d )
by Raffael
04:32
created

Router::setContentType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Micro
7
 *
8
 * @author      Raffael Sahli <[email protected]>
9
 * @copyright   Copryright (c) 2015-2017 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->type;
0 ignored issues
show
Bug Best Practice introduced by
The property type does not exist on Micro\Http\Router. Did you maybe forget to declare it?
Loading history...
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, 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->getClass().'::'.$callable[1].'], but callable 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) {
0 ignored issues
show
introduced by
The condition false === $match can never be false.
Loading history...
264
                throw new Exception($this->verb.' '.$this->path.' could not be routed, no matching routes found');
265
            }
266
            if ($response instanceof Response) {
267
                $this->logger->info('send http response ['.$response->getCode().']', [
268
                        'category' => get_class($this),
269
                    ]);
270
271
                $response->send();
272
            } else {
273
                $this->logger->debug('callback did not return a response, route exectuted successfully', [
274
                        'category' => get_class($this),
275
                    ]);
276
            }
277
278
            return true;
279
        } catch (\Exception $e) {
280
            return $this->sendException($e);
281
        }
282
    }
283
284
    /**
285
     * Sends a exception response to the client.
286
     *
287
     * @param \Exception $exception
288
     *
289
     * @return bool
290
     */
291
    public function sendException(\Exception $exception): bool
292
    {
293
        $message = $exception->getMessage();
294
        $class = get_class($exception);
295
296
        $msg = [
297
            'error' => $class,
298
            'message' => $message,
299
            'code' => $exception->getCode(),
300
        ];
301
302
        if (defined("$class::HTTP_CODE")) {
303
            $http_code = $class::HTTP_CODE;
304
        } else {
305
            $http_code = 500;
306
        }
307
308
        $this->logger->error('uncaught exception '.$message.']', [
309
            'category' => get_class($this),
310
            'exception' => $exception,
311
        ]);
312
313
        (new Response())
314
            ->setCode($http_code)
315
            ->setBody($msg)
316
            ->send();
317
318
        return true;
319
    }
320
321
    /**
322
     * Build method name.
323
     *
324
     * @param string $name
325
     *
326
     * @return string
327
     */
328
    protected function _buildMethodName(string $name): string
329
    {
330
        $result = $this->verb;
331
        $split = explode('-', $name);
332
        foreach ($split as $part) {
333
            $result .= ucfirst($part);
334
        }
335
336
        return $result;
337
    }
338
339
    /**
340
     * Check if method got params and combine these with
341
     * $_REQUEST.
342
     *
343
     * @param string $class
344
     * @param string $method
345
     * @param array  $parsed_params
346
     *
347
     * @return callable
348
     */
349
    protected function getParams(string $class, string $method, array $parsed_params): array
350
    {
351
        try {
352
            $return = [];
353
            $meta = new ReflectionMethod($class, $method);
354
            $params = $meta->getParameters();
355
            $json_params = [];
356
357
            if ('application/x-www-form-urlencoded' === $this->content_type) {
358
                $body = file_get_contents('php://input');
359
                parse_str($body, $decode);
360
                $request_params = array_merge($decode, $_REQUEST, $parsed_params);
361
            } elseif ('application/json' === $this->content_type) {
362
                $body = file_get_contents('php://input');
363
                if (!empty($body)) {
364
                    $json_params = json_decode($body, true);
365
                } else {
366
                    $parts = explode('&', $_SERVER['QUERY_STRING']);
367
                    if (!empty($parts)) {
368
                        $json_params = json_decode(urldecode($parts[0]), true);
369
                    }
370
                }
371
                if (null === $json_params) {
372
                    throw new Exception('invalid json input given');
373
                }
374
375
                $request_params = array_merge($json_params, $_REQUEST, $parsed_params);
376
            } else {
377
                $request_params = array_merge($parsed_params, $_REQUEST);
378
            }
379
380
            foreach ($params as $param) {
381
                $type = (string) $param->getType();
382
                $optional = $param->isOptional();
383
384
                if (isset($request_params[$param->name]) && '' !== $request_params[$param->name]) {
385
                    $param_value = $request_params[$param->name];
386
                } elseif (isset($json_params[$param->name])) {
387
                    $param_value = $json_params[$param->name];
388
                } elseif (true === $optional) {
389
                    $return[$param->name] = $param->getDefaultValue();
390
391
                    continue;
392
                } else {
393
                    $param_value = null;
394
                }
395
396
                if (null === $param_value && false === $optional) {
397
                    throw new Exception('misssing required parameter '.$param->name);
398
                }
399
400
                switch ($type) {
401
                    case 'bool':
402
                        if ('false' === $param_value) {
403
                            $return[$param->name] = false;
404
                        } else {
405
                            $return[$param->name] = (bool) $param_value;
406
                        }
407
408
                    break;
409
                    case 'int':
410
                        $return[$param->name] = (int) $param_value;
411
412
                    break;
413
                    case 'float':
414
                        $return[$param->name] = (float) $param_value;
415
416
                    break;
417
                    case 'array':
418
                        $return[$param->name] = (array) $param_value;
419
420
                    break;
421
                    default:
422
                        if (class_exists($type) && null !== $param_value) {
423
                            $return[$param->name] = new $type($param_value);
424
                        } else {
425
                            $return[$param->name] = $param_value;
426
                        }
427
428
                    break;
429
                }
430
            }
431
432
            return $return;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $return returns the type array<mixed,mixed>|array which is incompatible with the documented return type callable.
Loading history...
433
        } catch (ReflectionException $e) {
434
            throw new Exception('misssing or invalid required request parameter');
435
        }
436
    }
437
}
438