Completed
Pull Request — master (#389)
by Anton
05:06
created

Router   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 542
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 80.23%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
dl 0
loc 542
ccs 138
cts 172
cp 0.8023
rs 5.7894
c 2
b 0
f 0
wmc 65
lcom 1
cbo 8

25 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 48 8
A getBaseUrl() 0 4 1
A setBaseUrl() 0 4 1
A getParam() 0 4 1
A setParam() 0 10 4
A getParams() 0 4 1
A getRawParams() 0 4 1
A getDefaultModule() 0 4 1
A setDefaultModule() 0 4 1
A getDefaultController() 0 4 1
A setDefaultController() 0 4 1
A getErrorModule() 0 4 1
A setErrorModule() 0 4 1
A getErrorController() 0 4 1
A setErrorController() 0 4 1
B getUrl() 0 19 5
A getFullUrl() 0 10 1
B urlCustom() 0 29 5
C urlRoute() 0 29 7
A process() 0 17 4
A processDefault() 0 5 1
B processCustom() 0 18 5
C processRoute() 0 35 7
A resetRequest() 0 20 1
A getCleanUri() 0 11 4

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
/**
3
 * Bluz Framework Component
4
 *
5
 * @copyright Bluz PHP Team
6
 * @link https://github.com/bluzphp/framework
7
 */
8
9
/**
10
 * @namespace
11
 */
12
namespace Bluz\Router;
13
14
use Bluz\Application\Application;
15
use Bluz\Common\Options;
16
use Bluz\Controller\Controller;
17
use Bluz\Proxy\Cache;
18
use Bluz\Proxy\Request;
19
20
/**
21
 * Router
22
 *
23
 * @package  Bluz\Router
24
 * @author   Anton Shevchuk
25
 * @link     https://github.com/bluzphp/framework/wiki/Router
26
 */
27
class Router
28
{
29
    use Options;
30
31
    /**
32
     * Or should be as properties?
33
     */
34
    const DEFAULT_MODULE = 'index';
35
    const DEFAULT_CONTROLLER = 'index';
36
    const ERROR_MODULE = 'error';
37
    const ERROR_CONTROLLER = 'index';
38
39
    /**
40
     * @var string base URL
41
     */
42
    protected $baseUrl;
43
44
    /**
45
     * @var string REQUEST_URI minus Base URL
46
     */
47
    protected $cleanUri;
48
49
    /**
50
     * @var string default module
51
     */
52
    protected $defaultModule = self::DEFAULT_MODULE;
53
54
    /**
55
     * @var string default Controller
56
     */
57
    protected $defaultController = self::DEFAULT_CONTROLLER;
58
59
    /**
60
     * @var string error module
61
     */
62
    protected $errorModule = self::ERROR_MODULE;
63
64
    /**
65
     * @var string error Controller
66
     */
67
    protected $errorController = self::ERROR_CONTROLLER;
68
69
    /**
70
     * @var array instance parameters
71
     */
72
    protected $params = [];
73
74
    /**
75
     * @var array instance raw parameters
76
     */
77
    protected $rawParams = [];
78
79
    /**
80
     * @var array routers map
81
     */
82
    protected $routers = [];
83
84
    /**
85
     * @var array reverse map
86
     */
87
    protected $reverse = [];
88
89
    /**
90
     * Constructor of Router
91
     */
92 635
    public function __construct()
93
    {
94 635
        $routers = Cache::get('router.routers');
95 635
        $reverse = Cache::get('router.reverse');
96
97 635
        if (!$routers || !$reverse) {
98 635
            $routers = [];
99 635
            $reverse = [];
100 635
            $path = Application::getInstance()->getPath() . '/modules/*/controllers/*.php';
101 635
            foreach (new \GlobIterator($path) as $file) {
102
                /* @var \SplFileInfo $file */
103 635
                $module = $file->getPathInfo()->getPathInfo()->getBasename();
104 635
                $controller = $file->getBasename('.php');
105 635
                $controllerInstance = new Controller($module, $controller);
106 635
                $reflection = $controllerInstance->getReflection();
107 635
                if ($routes = $reflection->getRoute()) {
108 635
                    foreach ($routes as $route => $pattern) {
109 635
                        if (!isset($reverse[$module])) {
110 635
                            $reverse[$module] = [];
111
                        }
112
113 635
                        $reverse[$module][$controller] = ['route' => $route, 'params' => $reflection->getParams()];
114
115
                        $rule = [
116
                            $route => [
117 635
                                'pattern' => $pattern,
118 635
                                'module' => $module,
119 635
                                'controller' => $controller,
120 635
                                'params' => $reflection->getParams()
121
                            ]
122
                        ];
123
124
                        // static routers should be first
125 635
                        if (strpos($route, '$')) {
126 635
                            $routers = array_merge($routers, $rule);
127
                        } else {
128 635
                            $routers = array_merge($rule, $routers);
129
                        }
130
                    }
131
                }
132
            }
133 635
            Cache::set('router.routers', $routers, Cache::TTL_NO_EXPIRY, ['system']);
134 635
            Cache::set('router.reverse', $reverse, Cache::TTL_NO_EXPIRY, ['system']);
135
        }
136
137 635
        $this->routers = $routers;
138 635
        $this->reverse = $reverse;
139 635
    }
140
141
    /**
142
     * Get the base URL.
143
     *
144
     * @return string
145
     */
146 42
    public function getBaseUrl()
147
    {
148 42
        return $this->baseUrl;
149
    }
150
151
    /**
152
     * Set the base URL.
153
     *
154
     * @param  string $baseUrl
155
     * @return void
156
     */
157 635
    public function setBaseUrl($baseUrl)
158
    {
159 635
        $this->baseUrl = rtrim($baseUrl, '/') . '/';
160 635
    }
161
162
    /**
163
     * Get an action parameter
164
     *
165
     * @param  string $key
166
     * @param  mixed  $default Default value to use if key not found
167
     * @return mixed
168
     */
169
    public function getParam($key, $default = null)
170
    {
171
        return $this->params[$key] ?? $default;
172
    }
173
174
    /**
175
     * Set an action parameter
176
     *
177
     * A $value of null will unset the $key if it exists
178
     *
179
     * @param  string $key
180
     * @param  mixed  $value
181
     * @return void
182
     */
183 5
    public function setParam($key, $value)
184
    {
185 5
        $key = (string)$key;
186
187 5
        if ((null === $value) && isset($this->params[$key])) {
188
            unset($this->params[$key]);
189 5
        } elseif (null !== $value) {
190 5
            $this->params[$key] = $value;
191
        }
192 5
    }
193
194
    /**
195
     * Get parameters
196
     *
197
     * @return array
198
     */
199
    public function getParams()
200
    {
201
        return $this->params;
202
    }
203
204
    /**
205
     * Get raw params, w/out module and controller
206
     *
207
     * @return array
208
     */
209
    public function getRawParams()
210
    {
211
        return $this->rawParams;
212
    }
213
214
    /**
215
     * Get default module
216
     *
217
     * @return string
218
     */
219 44
    public function getDefaultModule()
220
    {
221 44
        return $this->defaultModule;
222
    }
223
224
    /**
225
     * Set default module
226
     *
227
     * @param  string $defaultModule
228
     * @return void
229
     */
230
    public function setDefaultModule($defaultModule)
231
    {
232
        $this->defaultModule = $defaultModule;
233
    }
234
235
    /**
236
     * Get default controller
237
     *
238
     * @return string
239
     */
240 44
    public function getDefaultController()
241
    {
242 44
        return $this->defaultController;
243
    }
244
245
    /**
246
     * Set default controller
247
     *
248
     * @param  string $defaultController
249
     * @return void
250
     */
251
    public function setDefaultController($defaultController)
252
    {
253
        $this->defaultController = $defaultController;
254
    }
255
256
    /**
257
     * Get error module
258
     *
259
     * @return string
260
     */
261 3
    public function getErrorModule()
262
    {
263 3
        return $this->errorModule;
264
    }
265
266
    /**
267
     * Set error module
268
     *
269
     * @param  string $errorModule
270
     * @return void
271
     */
272
    public function setErrorModule($errorModule)
273
    {
274
        $this->errorModule = $errorModule;
275
    }
276
277
    /**
278
     * Get error controller
279
     *
280
     * @return string
281
     */
282 3
    public function getErrorController()
283
    {
284 3
        return $this->errorController;
285
    }
286
287
    /**
288
     * Set error controller
289
     *
290
     * @param  string $errorController
291
     * @return void
292
     */
293
    public function setErrorController($errorController)
294
    {
295
        $this->errorController = $errorController;
296
    }
297
298
    /**
299
     * Build URL to controller
300
     *
301
     * @param  string $module
302
     * @param  string $controller
303
     * @param  array  $params
304
     * @return string
305
     */
306 34
    public function getUrl($module = self::DEFAULT_MODULE, $controller = self::DEFAULT_CONTROLLER, $params = [])
307
    {
308 34
        if (is_null($module)) {
309 1
            $module = Request::getModule();
310
        }
311
312 34
        if (is_null($controller)) {
313 1
            $controller = Request::getController();
314
        }
315
316 34
        if (empty($this->routers)) {
317
            return $this->urlRoute($module, $controller, $params);
318
        } else {
319 34
            if (isset($this->reverse[$module], $this->reverse[$module][$controller])) {
320 5
                return $this->urlCustom($module, $controller, $params);
321
            }
322 29
            return $this->urlRoute($module, $controller, $params);
323
        }
324
    }
325
326
    /**
327
     * Build full URL to controller
328
     *
329
     * @param  string $module
330
     * @param  string $controller
331
     * @param  array  $params
332
     * @return string
333
     */
334 1
    public function getFullUrl(
335
        $module = self::DEFAULT_MODULE,
336
        $controller = self::DEFAULT_CONTROLLER,
337
        $params = []
338
    ) {
339 1
        $scheme = Request::getInstance()->getUri()->getScheme() . '://';
340 1
        $host = Request::getInstance()->getUri()->getHost();
341 1
        $url = $this->getUrl($module, $controller, $params);
342 1
        return $scheme . $host . $url;
343
    }
344
345
    /**
346
     * Build URL by custom route
347
     *
348
     * @param  string $module
349
     * @param  string $controller
350
     * @param  array  $params
351
     * @return string
352
     */
353 5
    protected function urlCustom($module, $controller, $params)
354
    {
355 5
        $url = $this->reverse[$module][$controller]['route'];
356
357 5
        $getParams = [];
358 5
        foreach ($params as $key => $value) {
359
            // sub-array as GET params
360 4
            if (is_array($value)) {
361 1
                $getParams[$key] = $value;
362 1
                continue;
363
            }
364 3
            $url = str_replace('{$' . $key . '}', $value, $url, $replaced);
365
            // if not replaced, setup param as GET
366 3
            if (!$replaced) {
367 3
                $getParams[$key] = $value;
368
            }
369
        }
370
        // clean optional params
371 5
        $url = preg_replace('/\{\$[a-z0-9-_]+\}/i', '', $url);
372
        // clean regular expression (.*)
373 5
        $url = preg_replace('/\(\.\*\)/i', '', $url);
374
        // replace "//" with "/"
375 5
        $url = str_replace('//', '/', $url);
376
377 5
        if (!empty($getParams)) {
378 1
            $url .= '?' . http_build_query($getParams);
379
        }
380 5
        return $this->getBaseUrl() . ltrim($url, '/');
381
    }
382
383
    /**
384
     * Build URL by default route
385
     *
386
     * @param  string $module
387
     * @param  string $controller
388
     * @param  array  $params
389
     * @return string
390
     */
391 29
    protected function urlRoute($module, $controller, $params)
392
    {
393 29
        $url = $this->getBaseUrl();
394
395 29
        if (empty($params)) {
396 15
            if ($controller == self::DEFAULT_CONTROLLER) {
397 10
                if ($module == self::DEFAULT_MODULE) {
398 7
                    return $url;
399
                } else {
400 3
                    return $url . $module;
401
                }
402
            }
403
        }
404
405 20
        $url .= $module . '/' . $controller;
406 20
        $getParams = [];
407 20
        foreach ($params as $key => $value) {
408
            // sub-array as GET params
409 15
            if (is_array($value)) {
410 1
                $getParams[$key] = $value;
411 1
                continue;
412
            }
413 14
            $url .= '/' . urlencode($key) . '/' . urlencode($value);
414
        }
415 20
        if (!empty($getParams)) {
416 1
            $url .= '?' . http_build_query($getParams);
417
        }
418 20
        return $url;
419
    }
420
421
    /**
422
     * Process routing
423
     *
424
     * @return \Bluz\Router\Router
425
     */
426 6
    public function process()
427
    {
428
        switch (true) {
429
            // try process default router
430 6
            case $this->processDefault():
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
431 1
                break;
432
            // try process custom routers
433 5
            case $this->processCustom():
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
434
                break;
435
            // try process router
436 5
            case $this->processRoute():
437 5
                break;
438
        }
439
440 6
        $this->resetRequest();
441 6
        return $this;
442
    }
443
444
    /**
445
     * Process default router
446
     *
447
     * @return bool
448
     */
449 6
    protected function processDefault()
450
    {
451 6
        $uri = $this->getCleanUri();
452 6
        return empty($uri);
453
    }
454
455
    /**
456
     * Process custom router
457
     *
458
     * @return bool
459
     */
460 5
    protected function processCustom()
461
    {
462 5
        $uri = '/' . $this->getCleanUri();
463 5
        foreach ($this->routers as $router) {
464 5
            if (preg_match($router['pattern'], $uri, $matches)) {
465
                $this->setParam('_module', $router['module']);
466
                $this->setParam('_controller', $router['controller']);
467
468
                foreach ($router['params'] as $param => $type) {
469
                    if (isset($matches[$param])) {
470
                        $this->setParam($param, $matches[$param]);
471
                    }
472
                }
473 5
                return true;
474
            }
475
        }
476 5
        return false;
477
    }
478
479
    /**
480
     * Process router by default rules
481
     *
482
     * Default routers examples
483
     *     /
484
     *     /:module/
485
     *     /:module/:controller/
486
     *     /:module/:controller/:key1/:value1/:key2/:value2...
487
     *
488
     * @return bool
489
     */
490 5
    protected function processRoute()
491
    {
492 5
        $uri = $this->getCleanUri();
493 5
        $uri = trim($uri, '/');
494 5
        $raw = explode('/', $uri);
495
496
        // rewrite module from request
497 5
        if (sizeof($raw)) {
498 5
            $this->setParam('_module', array_shift($raw));
499
        }
500
        // rewrite module from controller
501 5
        if (sizeof($raw)) {
502 5
            $this->setParam('_controller', array_shift($raw));
503
        }
504 5
        if ($size = sizeof($raw)) {
505
            // save raw
506
            $this->rawParams = $raw;
507
508
            // save as index params
509
            foreach ($raw as $i => $value) {
510
                $this->setParam($i, $value);
511
            }
512
513
            // remove tail
514
            if ($size % 2 == 1) {
515
                array_pop($raw);
516
                $size = sizeof($raw);
517
            }
518
            // or use array_chunk and run another loop?
519
            for ($i = 0; $i < $size; $i = $i + 2) {
520
                $this->setParam($raw[$i], $raw[$i + 1]);
521
            }
522
        }
523 5
        return true;
524
    }
525
526
    /**
527
     * Reset Request
528
     *
529
     * @return void
530
     */
531 6
    protected function resetRequest()
532
    {
533 6
        $request = Request::getInstance();
534
535
        // priority:
536
        //  - default values
537
        //  - from GET query
538
        //  - from path
539 6
        $request = $request->withQueryParams(
540
            array_merge(
541
                [
542 6
                    '_module' => $this->getDefaultModule(),
543 6
                    '_controller' => $this->getDefaultController()
544
                ],
545 6
                $request->getQueryParams(),
546 6
                $this->params
547
            )
548
        );
549 6
        Request::setInstance($request);
550 6
    }
551
552
    /**
553
     * Get the request URI without baseUrl
554
     *
555
     * @return string
556
     */
557 6
    public function getCleanUri()
558
    {
559 6
        if ($this->cleanUri === null) {
560 6
            $uri = Request::getUri()->getPath();
561 6
            if ($this->getBaseUrl() && strpos($uri, $this->getBaseUrl()) === 0) {
562 6
                $uri = substr($uri, strlen($this->getBaseUrl()));
563
            }
564 6
            $this->cleanUri = $uri;
565
        }
566 6
        return $this->cleanUri;
567
    }
568
}
569