Router::getParams()   D
last analyzed

Complexity

Conditions 18
Paths 330

Size

Total Lines 64
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 64
rs 4.4452
c 0
b 0
f 0
cc 18
eloc 45
nc 330
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
16
use \Micro\Http\Router\Route;
17
use \ReflectionMethod;
18
use \ReflectionException;
19
use \Psr\Container\ContainerInterface;
20
21
class Router
22
{
23
    /**
24
     * Requested route
25
     *
26
     * @var string
27
     */
28
    protected $path;
29
30
31
    /**
32
     * HTTP verb
33
     *
34
     * @var string
35
     */
36
    protected $verb;
37
38
39
    /**
40
     * Installed routes
41
     *
42
     * @var array
43
     */
44
    protected $routes = [];
45
46
47
    /**
48
     * Logger
49
     *
50
     * @var Logger
51
     */
52
    protected $logger;
53
54
55
    /**
56
     * DI container
57
     *
58
     * @var ContainerInterface
59
     */
60
    protected $container;
61
62
63
    /**
64
     * Init router
65
     *
66
     * @param   LoggerInterface $logger
67
     * @param   ContainerInterface $container
68
     * @param   array $request
69
     * @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...
70
     */
71
    public function __construct(LoggerInterface $logger, ?array $request=null, ?ContainerInterface $container=null)
72
    {
73
        $this->logger = $logger;
0 ignored issues
show
Documentation Bug introduced by
It seems like $logger of type object<Psr\Log\LoggerInterface> is incompatible with the declared type object<Micro\Http\Logger> of property $logger.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
74
        $this->container = $container;
75
76
        if($request === null) {
77
            $request = $_SERVER;
78
        }
79
80
        if (isset($request['PATH_INFO'])) {
81
            $this->setPath($request['PATH_INFO']);
82
        }
83
84
        if (isset($request['REQUEST_METHOD'])) {
85
            $this->setVerb($request['REQUEST_METHOD']);
86
        }
87
    }
88
89
90
    /**
91
     * Add route to the beginning of the routing table
92
     *
93
     * @param   Route $route
94
     * @return  Router
95
     */
96
    public function prependRoute(Route $route): Router
97
    {
98
        array_unshift($this->routes, $route);
99
        $route->setRouter($this);
100
        return $this;
101
    }
102
103
104
    /**
105
     * Add route to the end of the routing table
106
     *
107
     * @param   Route $route
108
     * @return  Router
109
     */
110
    public function appendRoute(Route $route): Router
111
    {
112
        $this->routes[] = $route;
113
        $route->setRouter($this);
114
        return $this;
115
    }
116
117
118
    /**
119
     * Clear routing table
120
     *
121
     * @return Router
122
     */
123
    public function clearRoutingTable(): Router
124
    {
125
        $this->routes = [];
126
        return $this;
127
    }
128
129
130
    /**
131
     * Get active routes
132
     *
133
     * @return array
134
     */
135
    public function getRoutes(): array
136
    {
137
        return $this->routes;
138
    }
139
140
141
    /**
142
     * Set HTTP verb
143
     *
144
     * @param   string $verb
145
     * @return  Router
146
     */
147
    public function setVerb(string $verb): Router
148
    {
149
        $this->verb = strtolower($verb);
150
        return $this;
151
    }
152
153
154
    /**
155
     * Get http verb
156
     *
157
     * @return string
158
     */
159
    public function getVerb(): string
160
    {
161
        return $this->verb;
162
    }
163
164
165
    /**
166
     * Set routing path
167
     *
168
     * @param   string $path
169
     * @return  Router
170
     */
171
    public function setPath(string $path): Router
172
    {
173
        $path = rtrim(trim($path), '/');
174
        $this->path = (string)$path;
175
        return $this;
176
    }
177
178
179
    /**
180
     * Get path
181
     *
182
     * @return string
183
     */
184
    public function getPath(): string
185
    {
186
        return $this->path;
187
    }
188
189
190
    /**
191
     * Build method name
192
     *
193
     * @param   string $name
194
     * @return  string
195
     */
196
    protected function _buildMethodName(string $name): string
197
    {
198
        $result = $this->verb;
199
        $split = explode('-', $name);
200
        foreach ($split as $part) {
201
            $result .= ucfirst($part);
202
        }
203
204
        return $result;
205
    }
206
207
208
    /**
209
     * Execute router
210
     *
211
     * @return bool
212
     */
213
    public function run(): bool
214
    {
215
        $this->logger->info('execute requested route ['.$this->path.']', [
216
            'category' => get_class($this),
217
        ]);
218
219
        try {
220
            $match = false;
221
            foreach ($this->routes as $key => $route) {
222
                if ($route->match()) {
223
                    $callable = $route->getCallable($this->container);
224
225
                    if (is_callable($callable)) {
226
                        $match = true;
227
                        $this->logger->info('found matching route, execute ['.$route->getClass().'::'.$callable[1].']', [
228
                            'category' => get_class($this),
229
                        ]);
230
231
                        $params = $this->getParams($route->getClass(), $callable[1], $route->getParams());
232
                        $response = call_user_func_array($callable, $params);
233
234
                        if (!$route->continueAfterMatch()) {
235
                            break;
236
                        }
237
                    } else {
238
                        $this->logger->debug('found matching route ['.$route->getClass().'::'.$callable[1].'], but callable was not found', [
239
                            'category' => get_class($this),
240
                        ]);
241
                    }
242
                } else {
243
                    $this->logger->debug('requested path ['.$this->path.'] does not match route ['.$route->getPath().']', [
244
                        'category' => get_class($this),
245
                    ]);
246
                }
247
            }
248
249
            if ($match === false) {
250
                throw new Exception($this->verb.' '.$this->path.' could not be routed, no matching routes found');
251
            } else {
252
                if ($response instanceof Response) {
253
                    $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...
254
                        'category' => get_class($this),
255
                    ]);
256
257
                    $response->send();
258
                } else {
259
                    $this->logger->debug('callback did not return a response, route exectuted successfully', [
260
                        'category' => get_class($this),
261
                    ]);
262
                }
263
            }
264
265
            return true;
266
        } catch (\Exception $e) {
267
            return $this->sendException($e);
268
        }
269
    }
270
271
272
    /**
273
     * Sends a exception response to the client
274
     *
275
     * @param   \Exception $exception
276
     * @return  bool
277
     */
278
    public function sendException(\Exception $exception): bool
279
    {
280
        $message = $exception->getMessage();
281
        $class = get_class($exception);
282
283
        $msg = [
284
            'error'   => $class,
285
            'message' => $message,
286
            'code'    => $exception->getCode()
287
        ];
288
289
        if(defined("$class::HTTP_CODE")) {
290
            $http_code = $class::HTTP_CODE;
291
        } else {
292
            $http_code = 500;
293
        }
294
295
        $this->logger->error('uncaught exception '.$message.']', [
296
            'category' => get_class($this),
297
            'exception' => $exception,
298
        ]);
299
300
        (new Response())
301
            ->setCode($http_code)
302
            ->setBody($msg)
303
            ->send();
304
305
        return true;
306
    }
307
308
309
    /**
310
     * Check if method got params and combine these with
311
     * $_REQUEST
312
     *
313
     * @param   string $class
314
     * @param   string $method
315
     * @param   array $parsed_params
316
     * @return  callable
317
     */
318
    protected function getParams(string $class, string $method, array $parsed_params): array
319
    {
320
        try {
321
            $return      = [];
322
            $meta        = new ReflectionMethod($class, $method);
323
            $params      = $meta->getParameters();
324
            $json_params = [];
325
326
            if (isset($_SERVER['CONTENT_TYPE']) && $_SERVER['CONTENT_TYPE'] == 'application/json') {
327
                $body = file_get_contents('php://input');
328
                if (!empty($body)) {
329
                    $json_params = json_decode($body, true);
330
                } else {
331
                    $parts = explode('&', $_SERVER['QUERY_STRING']);
332
                    if (!empty($parts)) {
333
                        $json_params = json_decode(urldecode($parts[0]), true);
334
                    }
335
                }
336
                if ($json_params === null) {
337
                    throw new Exception('invalid json input given');
338
                }
339
340
                $request_params = array_merge($json_params, $parsed_params);
341
            } else {
342
                $request_params = array_merge($parsed_params, $_REQUEST);
343
            }
344
345
            foreach ($params as $param) {
346
                if ($optional = $param->isOptional()) {
347
                    $default = $param->getDefaultValue();
348
                } else {
349
                    $default = null;
350
                }
351
352
                if (isset($request_params[$param->name]) && $request_params[$param->name] !== '') {
353
                    if (is_bool($default)) {
354
                        if($request_params[$param->name] === 'false') {
355
                             $return[$param->name] = false;
356
                        } else {
357
                            $return[$param->name] = (bool)$request_params[$param->name];
358
                        }
359
                   } elseif (is_int($default)) {
360
                        $return[$param->name] = (int)$request_params[$param->name];
361
                    } elseif (is_array($default)) {
362
                        $return[$param->name] = (array)$request_params[$param->name];
363
                    } else {
364
                        $return[$param->name] = $request_params[$param->name];
365
                    }
366
                } elseif (isset($json_params[$param->name])) {
367
                    $return[$param->name] = $json_params[$param->name];
368
                } else {
369
                    $return[$param->name] = $default;
370
                }
371
372
                if ($return[$param->name] === null && $optional === false) {
373
                    throw new Exception('misssing required parameter '.$param->name);
374
                }
375
            }
376
377
            return $return;
378
        } catch (ReflectionException $e) {
379
            throw new Exception('misssing or invalid required request parameter');
380
        }
381
    }
382
}
383