Completed
Push — master ( 0ea9e7...aef2ed )
by Anton
14s
created

Router::__construct()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

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