Router   F
last analyzed

Complexity

Total Complexity 63

Size/Duplication

Total Lines 558
Duplicated Lines 0 %

Test Coverage

Coverage 79.65%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 158
dl 0
loc 558
ccs 137
cts 172
cp 0.7965
rs 3.36
c 6
b 0
f 0
wmc 63

26 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 3
A setErrorModule() 0 3 1
A setErrorController() 0 3 1
A setDefaultModule() 0 3 1
A setDefaultController() 0 3 1
A setBaseUrl() 0 3 1
A getUrl() 0 13 2
A getErrorController() 0 3 1
A getDefaultController() 0 3 1
A getErrorModule() 0 3 1
A getBaseUrl() 0 3 1
A getParam() 0 3 1
A getFullUrl() 0 13 3
A getParams() 0 3 1
A getDefaultModule() 0 3 1
A getRawParams() 0 3 1
A getCleanUri() 0 10 4
B prepareRouterData() 0 40 6
A processDefault() 0 4 1
B processRoute() 0 34 7
A process() 0 7 3
A urlCustom() 0 31 6
B urlRoute() 0 25 7
A resetRequest() 0 19 1
A processCustom() 0 17 5
A setParam() 0 8 2

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.

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
/**
4
 * Bluz Framework Component
5
 *
6
 * @copyright Bluz PHP Team
7
 * @link      https://github.com/bluzphp/framework
8
 */
9
10
declare(strict_types=1);
11
12
namespace Bluz\Router;
13
14
use Bluz\Application\Application;
15
use Bluz\Common\Exception\CommonException;
16
use Bluz\Common\Exception\ComponentException;
17
use Bluz\Common\Options;
18
use Bluz\Controller\Controller;
19
use Bluz\Controller\ControllerException;
20
use Bluz\Proxy\Cache;
21
use Bluz\Proxy\Request;
22
use ReflectionException;
23
24
/**
25
 * Router
26
 *
27
 * @package  Bluz\Router
28
 * @author   Anton Shevchuk
29
 * @link     https://github.com/bluzphp/framework/wiki/Router
30
 */
31
class Router
32
{
33
    use Options;
34
35
    /**
36
     * Or should be as properties?
37
     */
38
    private const DEFAULT_MODULE = 'index';
39
    private const DEFAULT_CONTROLLER = 'index';
40
    private const ERROR_MODULE = 'error';
41
    private const ERROR_CONTROLLER = 'index';
42
43
    /**
44
     * @var string base URL
45
     */
46
    protected $baseUrl;
47
48
    /**
49
     * @var string REQUEST_URI minus Base URL
50
     */
51
    protected $cleanUri;
52
53
    /**
54
     * @var string default module
55
     */
56
    protected $defaultModule = self::DEFAULT_MODULE;
57
58
    /**
59
     * @var string default Controller
60
     */
61
    protected $defaultController = self::DEFAULT_CONTROLLER;
62
63
    /**
64
     * @var string error module
65
     */
66
    protected $errorModule = self::ERROR_MODULE;
67
68
    /**
69
     * @var string error Controller
70
     */
71
    protected $errorController = self::ERROR_CONTROLLER;
72
73
    /**
74
     * @var array instance parameters
75
     */
76
    protected $params = [];
77
78
    /**
79
     * @var array instance raw parameters
80
     */
81
    protected $rawParams = [];
82
83
    /**
84
     * @var array[] routers map
85
     */
86
    protected $routers = [];
87
88
    /**
89
     * @var array[] reverse map
90
     */
91
    protected $reverse = [];
92
93
    /**
94
     * Constructor of Router
95
     */
96 587
    public function __construct()
97
    {
98 587
        $routers = Cache::get('router.routers');
99 587
        $reverse = Cache::get('router.reverse');
100
101 587
        if (!$routers || !$reverse) {
102 587
            [$routers, $reverse] = $this->prepareRouterData();
103 587
            Cache::set('router.routers', $routers, Cache::TTL_NO_EXPIRY, ['system']);
104 587
            Cache::set('router.reverse', $reverse, Cache::TTL_NO_EXPIRY, ['system']);
105
        }
106
107 587
        $this->routers = $routers;
108 587
        $this->reverse = $reverse;
109 587
    }
110
111
    /**
112
     * Initial routers data from controllers
113
     *
114
     * @return array[]
115
     * @throws CommonException
116
     * @throws ComponentException
117
     * @throws ControllerException
118
     * @throws ReflectionException
119
     */
120 587
    private function prepareRouterData(): array
121
    {
122 587
        $routers = [];
123 587
        $reverse = [];
124 587
        $path = Application::getInstance()->getPath() . '/modules/*/controllers/*.php';
125 587
        foreach (new \GlobIterator($path) as $file) {
126
            /* @var \SplFileInfo $file */
127 587
            $module = $file->getPathInfo()->getPathInfo()->getBasename();
128 587
            $controller = $file->getBasename('.php');
129 587
            $controllerInstance = new Controller($module, $controller);
130 587
            $meta = $controllerInstance->getMeta();
131 587
            if ($routes = $meta->getRoute()) {
132 587
                foreach ($routes as $route => $pattern) {
133 587
                    if (!isset($reverse[$module])) {
134 587
                        $reverse[$module] = [];
135
                    }
136
137 587
                    $reverse[$module][$controller] = ['route' => $route, 'params' => $meta->getParams()];
138
139
                    $rule = [
140
                        $route => [
141 587
                            'pattern' => $pattern,
142 587
                            'module' => $module,
143 587
                            'controller' => $controller,
144 587
                            'params' => $meta->getParams()
145
                        ]
146
                    ];
147
148
                    // static routers should be first, than routes with variables `$...`
149
                    // all routes begin with slash `/`
150 587
                    if (strpos($route, '$')) {
151 587
                        $routers[] = $rule;
152
                    } else {
153 587
                        array_unshift($routers, $rule);
154
                    }
155
                }
156
            }
157
        }
158 587
        $routers = array_merge(...$routers);
159 587
        return [$routers, $reverse];
160
    }
161
162
    /**
163
     * Get the base URL.
164
     *
165
     * @return string
166
     */
167 71
    public function getBaseUrl(): string
168
    {
169 71
        return $this->baseUrl;
170
    }
171
172
    /**
173
     * Set the base URL.
174
     *
175
     * @param string $baseUrl
176
     *
177
     * @return void
178
     */
179 587
    public function setBaseUrl(string $baseUrl): void
180
    {
181 587
        $this->baseUrl = str_trim_end($baseUrl, '/');
182 587
    }
183
184
    /**
185
     * Get an action parameter
186
     *
187
     * @param mixed $key
188
     * @param mixed $default Default value to use if key not found
189
     *
190
     * @return mixed
191
     */
192
    public function getParam($key, $default = null)
193
    {
194
        return $this->params[$key] ?? $default;
195
    }
196
197
    /**
198
     * Set an action parameter
199
     *
200
     * A $value of null will unset the $key if it exists
201
     *
202
     * @param mixed $key
203
     * @param mixed $value
204
     *
205
     * @return void
206
     */
207 5
    public function setParam($key, $value): void
208
    {
209 5
        $key = (string)$key;
210
211 5
        if (null === $value) {
212
            unset($this->params[$key]);
213
        } else {
214 5
            $this->params[$key] = $value;
215
        }
216 5
    }
217
218
    /**
219
     * Get parameters
220
     *
221
     * @return array
222
     */
223
    public function getParams(): array
224
    {
225
        return $this->params;
226
    }
227
228
    /**
229
     * Get raw params, w/out module and controller
230
     *
231
     * @return array
232
     */
233
    public function getRawParams(): array
234
    {
235
        return $this->rawParams;
236
    }
237
238
    /**
239
     * Get default module
240
     *
241
     * @return string
242
     */
243 46
    public function getDefaultModule(): string
244
    {
245 46
        return $this->defaultModule;
246
    }
247
248
    /**
249
     * Set default module
250
     *
251
     * @param string $defaultModule
252
     *
253
     * @return void
254
     */
255
    public function setDefaultModule(string $defaultModule): void
256
    {
257
        $this->defaultModule = $defaultModule;
258
    }
259
260
    /**
261
     * Get default controller
262
     *
263
     * @return string
264
     */
265 46
    public function getDefaultController(): string
266
    {
267 46
        return $this->defaultController;
268
    }
269
270
    /**
271
     * Set default controller
272
     *
273
     * @param string $defaultController
274
     *
275
     * @return void
276
     */
277
    public function setDefaultController(string $defaultController): void
278
    {
279
        $this->defaultController = $defaultController;
280
    }
281
282
    /**
283
     * Get error module
284
     *
285
     * @return string
286
     */
287 3
    public function getErrorModule(): string
288
    {
289 3
        return $this->errorModule;
290
    }
291
292
    /**
293
     * Set error module
294
     *
295
     * @param string $errorModule
296
     *
297
     * @return void
298
     */
299
    public function setErrorModule(string $errorModule): void
300
    {
301
        $this->errorModule = $errorModule;
302
    }
303
304
    /**
305
     * Get error controller
306
     *
307
     * @return string
308
     */
309 3
    public function getErrorController(): string
310
    {
311 3
        return $this->errorController;
312
    }
313
314
    /**
315
     * Set error controller
316
     *
317
     * @param string $errorController
318
     *
319
     * @return void
320
     */
321
    public function setErrorController(string $errorController): void
322
    {
323
        $this->errorController = $errorController;
324
    }
325
326
    /**
327
     * Get the request URI without baseUrl
328
     *
329
     * @return string
330
     */
331 34
    public function getCleanUri(): string
332
    {
333 34
        if ($this->cleanUri === null) {
334 34
            $uri = Request::getUri()->getPath();
335 34
            if ($this->getBaseUrl() && strpos($uri, $this->getBaseUrl()) === 0) {
336 34
                $uri = substr($uri, strlen($this->getBaseUrl()));
337
            }
338 34
            $this->cleanUri = $uri;
339
        }
340 34
        return $this->cleanUri;
341
    }
342
343
    /**
344
     * Build URL to controller
345
     *
346
     * @param string|null $module
347
     * @param string|null $controller
348
     * @param array $params
349
     *
350
     * @return string
351
     */
352 34
    public function getUrl(
353
        ?string $module = self::DEFAULT_MODULE,
354
        ?string $controller = self::DEFAULT_CONTROLLER,
355
        array $params = []
356
    ): string {
357 34
        $module = $module ?? Request::getModule();
358 34
        $controller = $controller ?? Request::getController();
359
360 34
        if (isset($this->reverse[$module][$controller])) {
361 5
            return $this->urlCustom($module, $controller, $params);
362
        }
363
364 29
        return $this->urlRoute($module, $controller, $params);
365
    }
366
367
    /**
368
     * Build full URL to controller
369
     *
370
     * @param string $module
371
     * @param string $controller
372
     * @param array  $params
373
     *
374
     * @return string
375
     */
376 4
    public function getFullUrl(
377
        string $module = self::DEFAULT_MODULE,
378
        string $controller = self::DEFAULT_CONTROLLER,
379
        array $params = []
380
    ): string {
381 4
        $scheme = Request::getUri()->getScheme() . '://';
382 4
        $host = Request::getUri()->getHost();
383 4
        $port = Request::getUri()->getPort();
384 4
        if ($port && !in_array($port, [80, 443], true)) {
385
            $host .= ':' . $port;
386
        }
387 4
        $url = $this->getUrl($module, $controller, $params);
388 4
        return $scheme . $host . $url;
389
    }
390
391
    /**
392
     * Build URL by custom route
393
     *
394
     * @param string $module
395
     * @param string $controller
396
     * @param array $params
397
     *
398
     * @return string
399
     */
400 5
    protected function urlCustom(string $module, string $controller, array $params): string
401
    {
402 5
        $url = $this->reverse[$module][$controller]['route'];
403
404 5
        $getParams = [];
405 5
        foreach ($params as $key => $value) {
406
            // sub-array as GET params
407 4
            if (is_array($value)) {
408 1
                $getParams[$key] = $value;
409 1
                continue;
410
            }
411 3
            if (is_numeric($value)) {
412 1
                $value = (string) $value;
413
            }
414 3
            $url = str_replace('{$' . $key . '}', $value, $url, $replaced);
415
            // if not replaced, setup param as GET
416 3
            if (!$replaced) {
417
                $getParams[$key] = $value;
418
            }
419
        }
420
        // clean optional params
421 5
        $url = preg_replace('/\{\$[a-z0-9-_]+\}/i', '', $url);
422
        // clean regular expression (.*)
423 5
        $url = preg_replace('/\(\.\*\)/', '', $url);
424
        // replace "//" with "/"
425 5
        $url = str_replace('//', '/', $url);
426
427 5
        if (!empty($getParams)) {
428 1
            $url .= '?' . http_build_query($getParams);
429
        }
430 5
        return $this->getBaseUrl() . ltrim($url, '/');
431
    }
432
433
    /**
434
     * Build URL by default route
435
     *
436
     * @param string $module
437
     * @param string $controller
438
     * @param array $params
439
     *
440
     * @return string
441
     */
442 29
    protected function urlRoute(string $module, string $controller, array $params): string
443
    {
444 29
        $url = $this->getBaseUrl();
445
446 29
        if (empty($params) && $controller === self::DEFAULT_CONTROLLER) {
447 10
            if ($module === self::DEFAULT_MODULE) {
448 7
                return $url;
449
            }
450 3
            return $url . $module;
451
        }
452
453 20
        $url .= $module . '/' . $controller;
454 20
        $getParams = [];
455 20
        foreach ($params as $key => $value) {
456
            // sub-array as GET params
457 15
            if (is_array($value)) {
458 1
                $getParams[$key] = $value;
459 1
                continue;
460
            }
461 14
            $url .= '/' . urlencode((string)$key) . '/' . urlencode((string)$value);
462
        }
463 20
        if (!empty($getParams)) {
464 1
            $url .= '?' . http_build_query($getParams);
465
        }
466 20
        return $url;
467
    }
468
469
    /**
470
     * Process routing
471
     *
472
     * @return void
473
     */
474 9
    public function process(): void
475
    {
476 9
        $this->processDefault() || // try to process default router (homepage)
477 5
        $this->processCustom() ||  //  or custom routers
478 5
        $this->processRoute();     //  or default router schema
479
480 9
        $this->resetRequest();
481 9
    }
482
483
    /**
484
     * Process default router
485
     *
486
     * @return bool
487
     */
488 9
    protected function processDefault(): bool
489
    {
490 9
        $uri = $this->getCleanUri();
491 9
        return empty($uri);
492
    }
493
494
    /**
495
     * Process custom router
496
     *
497
     * @return bool
498
     */
499 5
    protected function processCustom(): bool
500
    {
501 5
        $uri = '/' . $this->getCleanUri();
502 5
        foreach ($this->routers as $router) {
503 5
            if (preg_match($router['pattern'], $uri, $matches)) {
504
                $this->setParam('_module', $router['module']);
505
                $this->setParam('_controller', $router['controller']);
506
507
                foreach ($router['params'] as $param => $type) {
508
                    if (isset($matches[$param])) {
509
                        $this->setParam($param, $matches[$param]);
510
                    }
511
                }
512
                return true;
513
            }
514
        }
515 5
        return false;
516
    }
517
518
    /**
519
     * Process router by default rules
520
     *
521
     * Default routers examples
522
     *     /
523
     *     /:module/
524
     *     /:module/:controller/
525
     *     /:module/:controller/:key1/:value1/:key2/:value2...
526
     *
527
     * @return bool
528
     */
529 5
    protected function processRoute(): bool
530
    {
531 5
        $uri = $this->getCleanUri();
532 5
        $uri = trim($uri, '/');
533 5
        $raw = explode('/', $uri);
534
535
        // rewrite module from request
536 5
        if (count($raw)) {
537 5
            $this->setParam('_module', array_shift($raw));
538
        }
539
        // rewrite module from controller
540 5
        if (count($raw)) {
541 5
            $this->setParam('_controller', array_shift($raw));
542
        }
543 5
        if ($size = count($raw)) {
544
            // save raw
545
            $this->rawParams = $raw;
546
547
            // save as index params
548
            foreach ($raw as $i => $value) {
549
                $this->setParam($i, $value);
550
            }
551
552
            // remove tail
553
            if ($size % 2 === 1) {
554
                array_pop($raw);
555
                $size = count($raw);
556
            }
557
            // or use array_chunk and run another loop?
558
            for ($i = 0; $i < $size; $i += 2) {
559
                $this->setParam($raw[$i], $raw[$i + 1]);
560
            }
561
        }
562 5
        return true;
563
    }
564
565
    /**
566
     * Reset Request
567
     *
568
     * @return void
569
     */
570 9
    protected function resetRequest(): void
571
    {
572 9
        $request = Request::getInstance();
573
574
        // priority:
575
        //  - default values
576
        //  - from GET query
577
        //  - from path
578 9
        $request = $request->withQueryParams(
579 9
            array_merge(
580
                [
581 9
                    '_module' => $this->getDefaultModule(),
582 9
                    '_controller' => $this->getDefaultController()
583
                ],
584 9
                $request->getQueryParams(),
585 9
                $this->params
586
            )
587
        );
588 9
        Request::setInstance($request);
589 9
    }
590
}
591