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