Completed
Pull Request — master (#81)
by
unknown
03:04
created

Router   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 457
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 76.4%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 65
c 1
b 0
f 0
lcom 1
cbo 2
dl 0
loc 457
ccs 136
cts 178
cp 0.764
rs 5.7894

15 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 27 3
A getConfig() 0 4 1
A prepare() 0 13 1
A isURIClean() 0 17 4
B normalize() 0 12 5
B parseURI() 0 23 4
B normalizeURI() 0 22 5
B discoverRoute() 0 28 4
A uri() 0 13 2
C sortURISegments() 0 36 8
A addQueryString() 0 10 2
A removeQueryString() 0 6 1
B currentURL() 0 19 6
C baseURL() 0 27 7
C redirect() 0 51 12

How to fix   Complexity   

Complex Class

Complex classes like Router often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Router, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Zewa;
3
4
use Zewa\Exception\RouteException;
5
6
/**
7
 * Handles everything relating to URL/URI.
8
 *
9
 * @author Zechariah Walden<zech @ zewadesign.com>
10
 */
11
class Router
12
{
13
    /**
14
     * System routes
15
     *
16
     * @var object
17
     */
18
    private $routes;
19
20
    /**
21
     * The active module
22
     *
23
     * @var    string
24
     * @access public
25
     */
26
    public $module;
27
28
    /**
29
     * The active controller
30
     *
31
     * @var    string
32
     * @access public
33
     */
34
    public $controller;
35
36
    /**
37
     * The active method
38
     *
39
     * @var    string
40
     * @access public
41
     */
42
    public $method;
43
44
    /**
45
     * The base URL
46
     *
47
     * @var    string
48
     * @access public
49
     */
50
    public $baseURL;
51
52
    /**
53
     * Default module
54
     *
55
     * @var    string
56
     * @access public
57
     */
58
    public $defaultModule;
59
60
    /**
61
     * Default controller
62
     *
63
     * @var    string
64
     * @access public
65
     */
66
    public $defaultController;
67
68
    /**
69
     * Default method
70
     *
71
     * @var    string
72
     * @access public
73
     */
74
    public $defaultMethod;
75
76
    /**
77
     * Default uri
78
     *
79
     * @var    string
80
     * @access public
81
     */
82
    public $uri;
83
84
    /**
85
     * @var Config
86
     */
87
    public $config;
88
    /**
89
     * Load up some basic configuration settings.
90
     */
91
    public function __construct(Config $config)
92
    {
93
        $this->modules = $config->get('Modules');
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
94
        $this->routes = $config->get('Routes');
95
96
        $this->prepare();
97
        //@TODO: routing
98
        $uriChunks = $this->parseURI($this->uri);
99
100 28
        $params = array_slice($uriChunks, 3);
101
102 28
        // clear ending / with no value..
103
        if (!empty($params) && $params[0] === '') {
104 28
            $params = [];
105 28
        }
106 28
107
        $config->set('Routing', (object)[
108 28
            'module' => $uriChunks[0],
109 28
            'controller' => $uriChunks[1],
110 28
            'method' => $uriChunks[2],
111 28
            'params' => $params,
112
            'baseURL' => $this->baseURL,
113 28
            'currentURL' => $this->currentURL
114
        ]);
115
116
        $this->config = $config;
117 28
    }
118 28
119 28
    public function getConfig()
120
    {
121
        return $this->config;
122 28
    }
123
124 18
    /**
125
     * Set class defaults and normalized url/uri segments
126 18
     */
127 18
    private function prepare()
128
    {
129 18
        $this->defaultModule = $this->modules['defaultModule'];
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
130 18
        $defaultModule = $this->defaultModule;
131 18
        $this->defaultController = $this->modules[$defaultModule]['defaultController'];
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
132 18
        $this->defaultMethod = $this->modules[$defaultModule]['defaultMethod'];
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
133 18
134 18
        $normalizedURI = $this->normalizeURI();
135
        //check routes
136
        $this->uri = $this->uri($normalizedURI);
137 18
        $this->baseURL = $this->baseURL();
138
        $this->currentURL = $this->currentURL();
0 ignored issues
show
Bug introduced by
The property currentURL does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
139
    }
140 28
141
    /**
142 28
     * Checks if URL contains special characters not permissable/considered dangerous
143 19
     *
144
     * Safe: a-z, 0-9, :, _, [, ], +
145
     *
146 19
     * @param $uri
147 1
     * @param $uriChunks
148
     * @return bool
149 28
     */
150
    private function isURIClean($uri, $uriChunks)
151
    {
152 10
        if (!preg_match("/^[a-z0-9:_\/\.\[\]-]+$/i", $uri)
153
            || array_filter(
154 18
                $uriChunks,
155
                function ($uriChunk) {
156
                    if (strpos($uriChunk, '__') !== false) {
157
                        return true;
158
                    }
159 22
                }
160
            )
161 22
        ) {
162 11
            return false;
163 3
        } else {
164 8
            return true;
165
        }
166 4
    }
167
168
    //@TODO add Security class.
169 22
    private function normalize($data)
170
    {
171
        if (is_numeric($data)) {
172
            if (is_int($data) || ctype_digit(trim($data, '-'))) {
173
                $data = (int)$data;
174
            } elseif ($data === (string)(float)$data) {
175
                //@TODO: this needs work.. 9E26 converts to float
176
                $data = (float)$data;
177
            }
178
        }
179
        return $data;
180
    }
181
182 28
    /**
183
     * Parse and explode URI segments into chunks
184 28
     *
185 28
     * @access private
186 28
     *
187 28
     * @param string $uri
188 28
     *
189 28
     * @return array chunks of uri
190 22
     * @throws RouteException on disallowed characters
191
     */
192 28
    private function parseURI($uri)
193
    {
194 28
        $uriFragments = explode('/', $uri);
195
        $uriChunks = [];
196
        $params = [];
197 28
        $iteration = 0;
198
        foreach ($uriFragments as $location => $fragment) {
199 28
            if ($iteration > 2) {
200 10
                $params[] = $this->normalize(trim($fragment));
201
            } else {
202
                $uriChunks[] = trim($fragment);
203 18
            }
204
            $iteration++;
205
        }
206
207
        $result = array_merge($uriChunks, $params);
208
209
        if ($this->isURIClean($uri, $result) === false) {
210
            throw new RouteException('Invalid key characters.');
211
        }
212 28
213
        return $result;
214 28
    }
215
216 28
    /**
217 26
     * Normalize the $_SERVER vars for formatting the URI.
218
     *
219 4
     * @access private
220
     * @return string formatted/u/r/l
221
     */
222 28
    private function normalizeURI()
0 ignored issues
show
Coding Style introduced by
normalizeURI uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
223 2
    {
224
        if (!empty($_SERVER['PATH_INFO'])) {
225
            $normalizedURI = $_SERVER['PATH_INFO'];
226 28
        } elseif (!empty($_SERVER['REQUEST_URI'])) {
227
            $normalizedURI = $_SERVER['REQUEST_URI'];
228 28
        } else {
229 28
            $normalizedURI = false;
230
        }
231
232 28
        if ($normalizedURI === '/') {
233
            $normalizedURI = false;
234
        }
235 28
236
        $normalizedURI = ltrim(preg_replace('/\?.*/', '', $normalizedURI), '/');
237 28
238
        if (! empty($this->routes)) {
239 28
            $normalizedURI = $this->discoverRoute($normalizedURI);
240 28
        }
241 28
242
        return $normalizedURI;
243
    }
244
245
    private function discoverRoute($uri)
246
    {
247
        $routes = $this->routes;
248
249
        foreach ($routes as $route => $reroute) {
250 28
            $pattern = '/^(?i)' . str_replace('/', '\/', $route) . '$/';
251
            if (preg_match($pattern, $uri, $params)) {
252
                array_shift($params);
253
254 28
                $uri = $reroute;
255
256
                if (! empty($params)) {
257
                    $pat = '/(\$\d+)/';
258
                    $uri = preg_replace_callback(
259
                        $pat,
260
                        function () use (&$params) {
261 28
                            $first = $params[0];
262
                            array_shift($params);
263
                            return $first;
264
                        },
265
                        $reroute
266
                    );
267
                }
268
            }
269
        }
270 28
271
        return $uri;
272
    }
273 28
274 24
    /**
275 24
     * Normalize the $_SERVER vars for formatting the URI.
276
     *
277 4
     * @param $uri
278
     * @access public
279
     * @return string formatted/u/r/l
280 28
     */
281 28
    private function uri($uri)
282
    {
283
284 28
        if ($uri !== '') {
285
            $uriChunks = explode('/', filter_var(trim($uri), FILTER_SANITIZE_URL));
286 28
            $chunks = $this->sortURISegments($uriChunks);
287 28
        } else {
288 28
            $chunks = $this->sortURISegments();
289
        }
290 28
291 24
        $uri = ltrim(implode('/', $chunks), '/');
292
        return $uri;
293 24
    }
294 22
295 2
    private function sortURISegments($uriChunks = [])
296
    {
297
        $module = ucfirst(strtolower($this->defaultModule));
298
        $controller = ucfirst(strtolower($this->defaultController));
299 24
        $method = ucfirst(strtolower($this->defaultMethod));
300 22
301 22
        if (!empty($uriChunks)) {
302 22
            $module = ucfirst(strtolower($uriChunks[0]));
303
304 22
            if (!empty($uriChunks[1])) {
305 22
                $controller = ucfirst(strtolower($uriChunks[1]));
306 22
            } elseif (!empty($this->modules->$module->defaultController)) {
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
307 22
                $controller = $this->modules->$module->defaultController;
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
308
            }
309
310 2
            if (!empty($uriChunks[2])) {
311
                $method = ucfirst(strtolower($uriChunks[2]));
312
                $class = '\\App\\Modules\\' . $module . '\\Controllers\\' . $controller;
313
                $methodExist = method_exists($class, $method);
314 24
                
315
                if ($methodExist === false) {
316
                    if (!empty($this->modules->$module->defaultMethod)) {
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
317 28
                        $method = $this->modules->$module->defaultMethod;
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
318 28
                        array_unshift($uriChunks, null);
319
                    }
320
                }
321 2
            } elseif (!empty($this->modules->$module->defaultMethod)) {
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
322
                $method = $this->modules->$module->defaultMethod;
0 ignored issues
show
Bug introduced by
The property modules does not seem to exist. Did you mean module?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
323 2
            }
324 2
325 2
            unset($uriChunks[0], $uriChunks[1], $uriChunks[2]);
326 2
        }
327
328 1
        $return = [$module, $controller, $method];
329
        return array_merge($return, array_values($uriChunks));
330
    }
331
332 2
    private function addQueryString($url, $key, $value)
333
    {
334 2
        $url = preg_replace('/(.*)(\?|&)' . $key . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
335 2
        $url = substr($url, 0, -1);
336 2
        if (strpos($url, '?') === false) {
337
            return ($url . '?' . $key . '=' . $value);
338
        } else {
339
            return ($url . '&' . $key . '=' . $value);
340
        }
341
    }
342
343
    private function removeQueryString($url, $key)
344
    {
345 28
        $url = preg_replace('/(.*)(\?|&)' . $key . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
346
        $url = substr($url, 0, -1);
347 28
        return ($url);
348 2
    }
349 2
350
    /**
351 28
     * Return the currentURL w/ query strings
352 28
     *
353
     * @access public
354
     * @return string http://tld.com/formatted/u/r/l?q=bingo
355 28
     */
356 2
    public function currentURL($params = false)
0 ignored issues
show
Coding Style introduced by
currentURL uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
357 2
    {
358 2
        if (trim($_SERVER['REQUEST_URI']) === '/') {
359
            $url = $this->baseURL()
360
                   . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
361
        } else {
362 28
            $url = $this->baseURL($this->uri)
363
                   . (!empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : '');
364
        }
365
366
        if (!empty($params)) {
367
            foreach ($params as $key => $param) {
0 ignored issues
show
Bug introduced by
The expression $params of type boolean is not traversable.
Loading history...
368
                $url = $this->removeQueryString($url, $key);
369
                $url = $this->addQueryString($url, $key, $param);
370
            }
371 28
        }
372
373 28
        return $url;
374 28
    }
375 28
376 28
    /**
377
     * Return the baseURL
378 28
     *
379 27
     * @access public
380 28
     * @return string http://tld.com
381
     */
382 1
    public function baseURL($path = '')
0 ignored issues
show
Coding Style introduced by
baseURL uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
383
    {
384 27
        if (is_null($this->baseURL)) {
385
            $self = $_SERVER['PHP_SELF'];
386
            $server = $_SERVER['HTTP_HOST']
387 28
                      . rtrim(str_replace(strstr($self, 'index.php'), '', $self), '/');
388
389
            if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')
390 28
                || !empty($_SERVER['HTTP_X_FORWARDED_PROTO'])
391
                && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'
392 28
            ) {
393 28
                $protocol = 'https://';
394
            } else {
395
                $protocol = 'http://';
396 28
            }
397
398
            $this->baseURL = $protocol . $server;
399
        }
400
401
        $url = $this->baseURL;
402
403
        if ($path !== '') {
404
            $url .= '/' . $path;
405
        }
406
407
        return $url;
408
    }
409
410
    /**
411
     * Set optional status header, and redirect to provided URL
412
     *
413
     * @access public
414
     * @return bool
415
     */
416
    public function redirect($url = '/', $status = null)
417
    {
418
        $url = str_replace(array('\r', '\n', '%0d', '%0a'), '', $url);
419
420
        if (headers_sent()) {
421
            return false;
422
        }
423
424
        // trap session vars before redirect
425
        session_write_close();
426
427
        if (is_null($status)) {
428
            $status = '302';
429
        }
430
431
        // push a status to the browser if necessary
432
        if ((int)$status > 0) {
433
            switch ($status) {
434
                case '301':
435
                    $msg = '301 Moved Permanently';
436
                    break;
437
                case '307':
438
                    $msg = '307 Temporary Redirect';
439
                    break;
440
                // Using these below (except 302) would be an intentional misuse of the 'system'
441
                // Need to dig into the above comment @zech
442
                case '401':
443
                    $msg = '401 Access Denied';
444
                    break;
445
                case '403':
446
                    $msg = '403 Request Forbidden';
447
                    break;
448
                case '404':
449
                    $msg = '404 Not Found';
450
                    break;
451
                case '405':
452
                    $msg = '405 Method Not Allowed';
453
                    break;
454
                case '302':
455
                default:
456
                    $msg = '302 Found';
457
                    break; // temp redirect
458
            }
459
            if (isset($msg)) {
460
                header('HTTP/1.1 ' . $msg);
461
            }
462
        }
463
464
        $url = preg_replace('!^/*!', '', $url);
465
        header("Location: " . $url);
466
    }
467
}
468