Completed
Push — master ( 070a1e...ccce77 )
by Anton
12s
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 713
    public function __construct()
92
    {
93 713
        $routers = Cache::get('router.routers');
94 713
        $reverse = Cache::get('router.reverse');
95
96 713
        if (!$routers || !$reverse) {
97 713
            list($routers, $reverse) = $this->prepareRouterData();
98 713
            Cache::set('router.routers', $routers, Cache::TTL_NO_EXPIRY, ['system']);
99 713
            Cache::set('router.reverse', $reverse, Cache::TTL_NO_EXPIRY, ['system']);
100
        }
101
102 713
        $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 713
        $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 713
    }
105
106
    /**
107
     * Initial routers data from controllers
108
     *
109
     * @return array[]
110
     */
111 713
    private function prepareRouterData()
112
    {
113 713
        $routers = [];
114 713
        $reverse = [];
115 713
        $path = Application::getInstance()->getPath() . '/modules/*/controllers/*.php';
116 713
        foreach (new \GlobIterator($path) as $file) {
117
            /* @var \SplFileInfo $file */
118 713
            $module = $file->getPathInfo()->getPathInfo()->getBasename();
119 713
            $controller = $file->getBasename('.php');
120 713
            $controllerInstance = new Controller($module, $controller);
121 713
            $meta = $controllerInstance->getMeta();
122 713
            if ($routes = $meta->getRoute()) {
123 713
                foreach ($routes as $route => $pattern) {
124 713
                    if (!isset($reverse[$module])) {
125 713
                        $reverse[$module] = [];
126
                    }
127
128 713
                    $reverse[$module][$controller] = ['route' => $route, 'params' => $meta->getParams()];
129
130
                    $rule = [
131
                        $route => [
132 713
                            'pattern' => $pattern,
133 713
                            'module' => $module,
134 713
                            'controller' => $controller,
135 713
                            'params' => $meta->getParams()
136
                        ]
137
                    ];
138
139
                    // static routers should be first, than routes with variables `$...`
140
                    // all routes begin with slash `/`
141 713
                    if (strpos($route, '$')) {
142 713
                        $routers[] = $rule;
143
                    } else {
144 713
                        array_unshift($routers, $rule);
145
                    }
146
                }
147
            }
148
        }
149 713
        $routers = array_merge(...$routers);
150 713
        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
     * @return void
168
     */
169 713
    public function setBaseUrl($baseUrl)
170
    {
171 713
        $this->baseUrl = str_trim_end($baseUrl, '/');
172 713
    }
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(
319
        $module = self::DEFAULT_MODULE,
320
        $controller = self::DEFAULT_CONTROLLER,
321
        array $params = []
322
    ) {
323 34
        $module = $module ?? Request::getModule();
324 34
        $controller = $controller ?? Request::getController();
325
326 34
        if (isset($this->reverse[$module], $this->reverse[$module][$controller])) {
327 5
            return $this->urlCustom($module, $controller, $params);
328
        }
329
330 29
        return $this->urlRoute($module, $controller, $params);
331
    }
332
333
    /**
334
     * Build full URL to controller
335
     *
336
     * @param  string $module
337
     * @param  string $controller
338
     * @param  array  $params
339
     * @return string
340
     */
341 1
    public function getFullUrl(
342
        $module = self::DEFAULT_MODULE,
343
        $controller = self::DEFAULT_CONTROLLER,
344
        array $params = []
345
    ) {
346 1
        $scheme = Request::getInstance()->getUri()->getScheme() . '://';
347 1
        $host = Request::getInstance()->getUri()->getHost();
348 1
        $url = $this->getUrl($module, $controller, $params);
349 1
        return $scheme . $host . $url;
350
    }
351
352
    /**
353
     * Build URL by custom route
354
     *
355
     * @param  string $module
356
     * @param  string $controller
357
     * @param  array  $params
358
     * @return string
359
     */
360 5
    protected function urlCustom($module, $controller, $params)
361
    {
362 5
        $url = $this->reverse[$module][$controller]['route'];
363
364 5
        $getParams = [];
365 5
        foreach ($params as $key => $value) {
366
            // sub-array as GET params
367 4
            if (is_array($value)) {
368 1
                $getParams[$key] = $value;
369 1
                continue;
370
            }
371 3
            $url = str_replace('{$' . $key . '}', $value, $url, $replaced);
372
            // if not replaced, setup param as GET
373 3
            if (!$replaced) {
374 3
                $getParams[$key] = $value;
375
            }
376
        }
377
        // clean optional params
378 5
        $url = preg_replace('/\{\$[a-z0-9-_]+\}/i', '', $url);
379
        // clean regular expression (.*)
380 5
        $url = preg_replace('/\(\.\*\)/', '', $url);
381
        // replace "//" with "/"
382 5
        $url = str_replace('//', '/', $url);
383
384 5
        if (!empty($getParams)) {
385 1
            $url .= '?' . http_build_query($getParams);
386
        }
387 5
        return $this->getBaseUrl() . ltrim($url, '/');
388
    }
389
390
    /**
391
     * Build URL by default route
392
     *
393
     * @param  string $module
394
     * @param  string $controller
395
     * @param  array  $params
396
     * @return string
397
     */
398 29
    protected function urlRoute($module, $controller, $params)
399
    {
400 29
        $url = $this->getBaseUrl();
401
402 29
        if (empty($params)) {
403 15
            if ($controller === self::DEFAULT_CONTROLLER) {
404 10
                if ($module === self::DEFAULT_MODULE) {
405 7
                    return $url;
406
                }
407 3
                return $url . $module;
408
            }
409
        }
410
411 20
        $url .= $module . '/' . $controller;
412 20
        $getParams = [];
413 20
        foreach ($params as $key => $value) {
414
            // sub-array as GET params
415 15
            if (is_array($value)) {
416 1
                $getParams[$key] = $value;
417 1
                continue;
418
            }
419 14
            $url .= '/' . urlencode((string)$key) . '/' . urlencode((string)$value);
420
        }
421 20
        if (!empty($getParams)) {
422 1
            $url .= '?' . http_build_query($getParams);
423
        }
424 20
        return $url;
425
    }
426
427
    /**
428
     * Process routing
429
     *
430
     * @return \Bluz\Router\Router
431
     */
432 6
    public function process()
433
    {
434 6
        $this->processDefault() || // try to process default router (homepage)
435 5
        $this->processCustom() ||  //  or custom routers
436 5
        $this->processRoute();     //  or default router schema
437
438 6
        $this->resetRequest();
439 6
        return $this;
440
    }
441
442
    /**
443
     * Process default router
444
     *
445
     * @return bool
446
     */
447 6
    protected function processDefault() : bool
448
    {
449 6
        $uri = $this->getCleanUri();
450 6
        return empty($uri);
451
    }
452
453
    /**
454
     * Process custom router
455
     *
456
     * @return bool
457
     */
458 5
    protected function processCustom() : bool
459
    {
460 5
        $uri = '/' . $this->getCleanUri();
461 5
        foreach ($this->routers as $router) {
462 5
            if (preg_match($router['pattern'], $uri, $matches)) {
463
                $this->setParam('_module', $router['module']);
464
                $this->setParam('_controller', $router['controller']);
465
466
                foreach ($router['params'] as $param => $type) {
467
                    if (isset($matches[$param])) {
468
                        $this->setParam($param, $matches[$param]);
469
                    }
470
                }
471 5
                return true;
472
            }
473
        }
474 5
        return false;
475
    }
476
477
    /**
478
     * Process router by default rules
479
     *
480
     * Default routers examples
481
     *     /
482
     *     /:module/
483
     *     /:module/:controller/
484
     *     /:module/:controller/:key1/:value1/:key2/:value2...
485
     *
486
     * @return bool
487
     */
488 5
    protected function processRoute() : bool
489
    {
490 5
        $uri = $this->getCleanUri();
491 5
        $uri = trim($uri, '/');
492 5
        $raw = explode('/', $uri);
493
494
        // rewrite module from request
495 5
        if (count($raw)) {
496 5
            $this->setParam('_module', array_shift($raw));
497
        }
498
        // rewrite module from controller
499 5
        if (count($raw)) {
500 5
            $this->setParam('_controller', array_shift($raw));
501
        }
502 5
        if ($size = count($raw)) {
503
            // save raw
504
            $this->rawParams = $raw;
505
506
            // save as index params
507
            foreach ($raw as $i => $value) {
508
                $this->setParam($i, $value);
509
            }
510
511
            // remove tail
512
            if ($size % 2 == 1) {
513
                array_pop($raw);
514
                $size = count($raw);
515
            }
516
            // or use array_chunk and run another loop?
517
            for ($i = 0; $i < $size; $i += 2) {
518
                $this->setParam($raw[$i], $raw[$i + 1]);
519
            }
520
        }
521 5
        return true;
522
    }
523
524
    /**
525
     * Reset Request
526
     *
527
     * @return void
528
     */
529 6
    protected function resetRequest()
530
    {
531 6
        $request = Request::getInstance();
532
533
        // priority:
534
        //  - default values
535
        //  - from GET query
536
        //  - from path
537 6
        $request = $request->withQueryParams(
538
            array_merge(
539
                [
540 6
                    '_module' => $this->getDefaultModule(),
541 6
                    '_controller' => $this->getDefaultController()
542
                ],
543 6
                $request->getQueryParams(),
544 6
                $this->params
545
            )
546
        );
547 6
        Request::setInstance($request);
548 6
    }
549
550
    /**
551
     * Get the request URI without baseUrl
552
     *
553
     * @return string
554
     */
555 17
    public function getCleanUri()
556
    {
557 17
        if ($this->cleanUri === null) {
558 17
            $uri = Request::getUri()->getPath();
559 17
            if ($this->getBaseUrl() && strpos($uri, $this->getBaseUrl()) === 0) {
560 17
                $uri = substr($uri, strlen($this->getBaseUrl()));
561
            }
562 17
            $this->cleanUri = $uri;
563
        }
564 17
        return $this->cleanUri;
565
    }
566
}
567