Completed
Push — master ( 091d0a...0daa93 )
by Raffael
01:36
created

Router::appendRoute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 1
1
<?php
2
declare(strict_types = 1);
3
4
/**
5
 * Micro
6
 *
7
 * @author    Raffael Sahli <[email protected]>
8
 * @copyright Copyright (c) 2017 gyselroth GmbH (https://gyselroth.com)
9
 * @license   MIT https://opensource.org/licenses/MIT
10
 */
11
12
namespace Micro\Http;
13
14
use \Micro\Helper;
15
use \Psr\Log\LoggerInterface as Logger;
16
use \Micro\Http\Router\Route;
17
use \ReflectionMethod;
18
use \ReflectionException;
19
20
class Router
21
{
22
    /**
23
     * Requested route
24
     *
25
     * @var string
26
     */
27
    protected $path;
28
29
30
    /**
31
     * HTTP verb
32
     *
33
     * @var string
34
     */
35
    protected $verb;
36
37
38
    /**
39
     * Installed routes
40
     *
41
     * @var array
42
     */
43
    protected $routes = [];
44
45
46
    /**
47
     * Logger
48
     *
49
     * @var Logger
50
     */
51
    protected $logger;
52
53
54
    /**
55
     * Init router
56
     *
57
     * @param   array $server
58
     * @param   Logger $logger
59
     * @return  void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
60
     */
61
    public function __construct(array $server, Logger $logger)
62
    {
63
        $this->logger = $logger;
64
        
65
        if (isset($server['PATH_INFO'])) {
66
            $this->setPath($server['PATH_INFO']);
67
        }
68
        
69
        if (isset($server['REQUEST_METHOD'])) {
70
            $this->setVerb($server['REQUEST_METHOD']);
71
        }
72
    }
73
74
    /**
75
     * Add route to the beginning of the routing table
76
     *
77
     * @param   Route $route
78
     * @return  Router
79
     */
80
    public function prependRoute(Route $route): Router
81
    {
82
        array_unshift($this->routes, $route);
83
        $route->setRouter($this);
84
        return $this;
85
    }
86
87
88
    /**
89
     * Add route to the end of the routing table
90
     *
91
     * @param   Route $route
92
     * @return  Router
93
     */
94
    public function appendRoute(Route $route): Router
95
    {
96
        $this->routes[] = $route;
97
        $route->setRouter($this);
98
        return $this;
99
    }
100
101
    
102
    /**
103
     * Clear routing table
104
     *
105
     * @return Router
106
     */
107
    public function clearRoutingTable(): Router
108
    {
109
        $this->routes = [];
110
        return $this;
111
    }
112
113
    
114
    /**
115
     * Get active routes
116
     *
117
     * @return array
118
     */
119
    public function getRoutes(): array
120
    {
121
        return $this->routes;
122
    }
123
    
124
    
125
    /**
126
     * Set HTTP verb
127
     *
128
     * @param   string $verb
129
     * @return  Router
130
     */
131
    public function setVerb(string $verb): Router
132
    {
133
        $this->verb = strtolower($verb);
134
        return $this;
135
    }
136
137
138
    /**
139
     * Get http verb
140
     *
141
     * @return string
142
     */
143
    public function getVerb(): string
144
    {
145
        return $this->verb;
146
    }
147
148
149
    /**
150
     * Set routing path
151
     *
152
     * @param   string $path
153
     * @return  Router
154
     */
155
    public function setPath(string $path): Router
156
    {
157
        $path = rtrim(trim($path), '/');
158
        $this->path = (string)$path;
159
        return $this;
160
    }
161
162
163
    /**
164
     * Get path
165
     *
166
     * @return string
167
     */
168
    public function getPath(): string
169
    {
170
        return $this->path;
171
    }
172
 
173
174
    /**
175
     * Build method name
176
     *
177
     * @param   string $name
178
     * @return  string
179
     */
180
    protected function _buildMethodName(string $name): string
181
    {
182
        $result = $this->verb;
183
        $split = explode('-', $name);
184
        foreach ($split as $part) {
185
            $result .= ucfirst($part);
186
        }
187
188
        return $result;
189
    }
190
191
192
    /**
193
     * Execute router
194
     *
195
     * @param  array $constructor
196
     * @return bool
197
     */
198
    public function run(array $constructor = []): bool
199
    {
200
        $this->logger->info('execute requested route ['.$this->path.']', [
201
            'category' => get_class($this),
202
        ]);
203
        
204
        try {
205
            $match = false;
206
            foreach ($this->routes as $key => $route) {
207
                if ($route->match()) {
208
                    $callable = $route->getCallable($constructor);
209
                    
210
                    if (is_callable($callable)) {
211
                        $match = true;
212
                        $this->logger->info('found matching route, execute ['.$route->getClass().'::'.$callable[1].']', [
213
                            'category' => get_class($this),
214
                        ]);
215
216
                        $params = $this->getParams($route->getClass(), $callable[1], $route->getParams());
217
                        $response = call_user_func_array($callable, $params);
218
                        
219
                        if (!$route->continueAfterMatch()) {
220
                            break;
221
                        }
222
                    } else {
223
                        throw new Exception('found matching route ['.$route->getClass().'::'.$callable[1].'], but callable was not found');
224
                    }
225
                } else {
226
                    $this->logger->debug('requested path ['.$this->path.'] does not match route ['.$route->getPath().']', [
227
                        'category' => get_class($this),
228
                    ]);
229
                }
230
            }
231
            
232
            if ($match === false) {
233
                throw new Exception($this->verb.' '.$this->path.' could not be routed, no matching routes found');
234
            } else {
235
                if ($response instanceof Response) {
236
                    $this->logger->info('send http response ['.$response->getCode().']', [
0 ignored issues
show
Bug introduced by
The variable $response does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
237
                        'category' => get_class($this),
238
                    ]);
239
240
                    $response->send();
241
                } else {
242
                    $this->logger->debug('callback did not return a response, route exectuted successfully', [
243
                        'category' => get_class($this),
244
                    ]);
245
                }
246
            }
247
248
            return true;
249
        } catch (\Exception $e) {
250
            return $this->sendException($e);
251
        }
252
    }
253
254
255
    /**
256
     * Sends a exception response to the client
257
     *
258
     * @param   \Exception $exception
259
     * @return  void
260
     */
261
    public function sendException(\Exception $exception): void
262
    {
263
        $message = $exception->getMessage();
264
        $msg = [
265
            'error'   => get_class($exception),
266
            'message' => $message,
267
            'code'    => $exception->getCode()
268
        ];
269
270
        $this->logger->error('uncaught exception '.$message.']', [
271
            'category' => get_class($this),
272
            'exception' => $exception,
273
        ]);
274
        
275
        (new Response())
276
            ->setCode(500)
277
            ->setBody($msg)
278
            ->send();
279
    }
280
281
282
    /**
283
     * Check if method got params and combine these with
284
     * $_REQUEST
285
     *
286
     * @param   string $class
287
     * @param   string $method
288
     * @param   array $parsed_params
289
     * @return  callable
290
     */
291
    protected function getParams(string $class, string $method, array $parsed_params): array
292
    {
293
        try {
294
            $return      = [];
295
            $meta        = new ReflectionMethod($class, $method);
296
            $params      = $meta->getParameters();
297
            $json_params = [];
298
            
299
            if (isset($_SERVER['CONTENT_TYPE']) && $_SERVER['CONTENT_TYPE'] == 'application/json') {
300
                $body = file_get_contents('php://input');
301
                if (!empty($body)) {
302
                    $json_params = json_decode($body, true);
303
                } else {
304
                    $parts = explode('&', $_SERVER['QUERY_STRING']);
305
                    if (!empty($parts)) {
306
                        $json_params = json_decode(urldecode($parts[0]), true);
307
                    }
308
                }
309
                if ($json_params === null) {
310
                    throw new Exception('invalid json input given');
311
                }
312
313
                $request_params = array_merge($json_params, $parsed_params);
314
            } else {
315
                $request_params = array_merge($parsed_params, $_REQUEST);
316
            }
317
            
318
            foreach ($params as $param) {
319
                if ($optional = $param->isOptional()) {
320
                    $default = $param->getDefaultValue();
321
                } else {
322
                    $default = null;
323
                }
324
325
                if (isset($request_params[$param->name]) && $request_params[$param->name] !== '') {
326
                    if (is_bool($default)) {
327
                        $return[$param->name] = Helper::boolParam($request_params[$param->name]);
328
                    } elseif (is_int($default)) {
329
                        $return[$param->name] = (int)$request_params[$param->name];
330
                    } elseif (is_array($default)) {
331
                        $return[$param->name] = (array)$request_params[$param->name];
332
                    } else {
333
                        $return[$param->name] = $request_params[$param->name];
334
                    }
335
                } elseif (isset($json_params[$param->name])) {
336
                    $return[$param->name] = $json_params[$param->name];
337
                } else {
338
                    $return[$param->name] = $default;
339
                }
340
341
                if ($return[$param->name] === null && $optional === false) {
342
                    throw new Exception('misssing required parameter '.$param->name);
343
                }
344
            }
345
            
346
            return $return;
347
        } catch (ReflectionException $e) {
348
            throw new Exception('misssing or invalid required request parameter');
349
        }
350
    }
351
}
352