Completed
Pull Request — master (#422)
by Anton
04:34
created

Router::process()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 0
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 3
rs 9.6666
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 712
    public function __construct()
92
    {
93 712
        $routers = Cache::get('router.routers');
94 712
        $reverse = Cache::get('router.reverse');
95
96 712
        if (!$routers || !$reverse) {
97 712
            list($routers, $reverse) = $this->prepareRouterData();
98 712
            Cache::set('router.routers', $routers, Cache::TTL_NO_EXPIRY, ['system']);
99 712
            Cache::set('router.reverse', $reverse, Cache::TTL_NO_EXPIRY, ['system']);
100
        }
101
102 712
        $this->routers = $routers;
0 ignored issues
show
Documentation Bug introduced by
It seems like $routers of type * is incompatible with the declared type array<integer,array> of property $routers.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
103 712
        $this->reverse = $reverse;
0 ignored issues
show
Documentation Bug introduced by
It seems like $reverse of type * is incompatible with the declared type array<integer,array> of property $reverse.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

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