Completed
Pull Request — master (#52)
by
unknown
03:05
created

Router::getParams()   F

Complexity

Conditions 19
Paths 698

Size

Total Lines 70
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 70
rs 2.8571
c 0
b 0
f 0
cc 19
eloc 51
nc 698
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
 * Balloon
6
 *
7
 * @author      Raffael Sahli <[email protected]>
8
 * @copyright   Copryright (c) 2012-2017 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPLv3 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Http;
13
14
use Balloon\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\Http\Exception.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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