Completed
Push — master ( 83cea8...e47c19 )
by Anton
17s queued 12s
created

Router::urlCustom()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5.0061

Importance

Changes 0
Metric Value
eloc 15
nc 8
nop 3
dl 0
loc 28
ccs 15
cts 16
cp 0.9375
c 0
b 0
f 0
cc 5
crap 5.0061
rs 9.4555
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 604
    public function __construct()
97
    {
98 604
        $routers = Cache::get('router.routers');
99 604
        $reverse = Cache::get('router.reverse');
100
101 604
        if (!$routers || !$reverse) {
102 604
            [$routers, $reverse] = $this->prepareRouterData();
103 604
            Cache::set('router.routers', $routers, Cache::TTL_NO_EXPIRY, ['system']);
104 604
            Cache::set('router.reverse', $reverse, Cache::TTL_NO_EXPIRY, ['system']);
105
        }
106
107 604
        $this->routers = $routers;
108 604
        $this->reverse = $reverse;
109 604
    }
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 604
    private function prepareRouterData()
121
    {
122 604
        $routers = [];
123 604
        $reverse = [];
124 604
        $path = Application::getInstance()->getPath() . '/modules/*/controllers/*.php';
125 604
        foreach (new \GlobIterator($path) as $file) {
126
            /* @var \SplFileInfo $file */
127 604
            $module = $file->getPathInfo()->getPathInfo()->getBasename();
128 604
            $controller = $file->getBasename('.php');
129 604
            $controllerInstance = new Controller($module, $controller);
130 604
            $meta = $controllerInstance->getMeta();
131 604
            if ($routes = $meta->getRoute()) {
132 604
                foreach ($routes as $route => $pattern) {
133 604
                    if (!isset($reverse[$module])) {
134 604
                        $reverse[$module] = [];
135
                    }
136
137 604
                    $reverse[$module][$controller] = ['route' => $route, 'params' => $meta->getParams()];
138
139
                    $rule = [
140
                        $route => [
141 604
                            'pattern' => $pattern,
142 604
                            'module' => $module,
143 604
                            'controller' => $controller,
144 604
                            'params' => $meta->getParams()
145
                        ]
146
                    ];
147
148
                    // static routers should be first, than routes with variables `$...`
149
                    // all routes begin with slash `/`
150 604
                    if (strpos($route, '$')) {
151 604
                        $routers[] = $rule;
152
                    } else {
153 604
                        array_unshift($routers, $rule);
154
                    }
155
                }
156
            }
157
        }
158 604
        $routers = array_merge(...$routers);
159 604
        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 604
    public function setBaseUrl($baseUrl): void
180
    {
181 604
        $this->baseUrl = str_trim_end($baseUrl, '/');
182 604
    }
183
184
    /**
185
     * Get an action parameter
186
     *
187
     * @param  string $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  string $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) && isset($this->params[$key])) {
212
            unset($this->params[$key]);
213 5
        } elseif (null !== $value) {
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($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($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($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($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 $module
347
     * @param  string $controller
348
     * @param  array  $params
349
     *
350
     * @return string
351
     */
352 34
    public function getUrl(
353
        $module = self::DEFAULT_MODULE,
354
        $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
        $module = self::DEFAULT_MODULE,
378
        $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)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $port of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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($module, $controller, $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
            $url = str_replace('{$' . $key . '}', $value, $url, $replaced);
412
            // if not replaced, setup param as GET
413 3
            if (!$replaced) {
414
                $getParams[$key] = $value;
415
            }
416
        }
417
        // clean optional params
418 5
        $url = preg_replace('/\{\$[a-z0-9-_]+\}/i', '', $url);
419
        // clean regular expression (.*)
420 5
        $url = preg_replace('/\(\.\*\)/', '', $url);
421
        // replace "//" with "/"
422 5
        $url = str_replace('//', '/', $url);
423
424 5
        if (!empty($getParams)) {
425 1
            $url .= '?' . http_build_query($getParams);
426
        }
427 5
        return $this->getBaseUrl() . ltrim($url, '/');
428
    }
429
430
    /**
431
     * Build URL by default route
432
     *
433
     * @param  string $module
434
     * @param  string $controller
435
     * @param  array  $params
436
     *
437
     * @return string
438
     */
439 29
    protected function urlRoute($module, $controller, $params): string
440
    {
441 29
        $url = $this->getBaseUrl();
442
443 29
        if (empty($params) && $controller === self::DEFAULT_CONTROLLER) {
444 10
            if ($module === self::DEFAULT_MODULE) {
445 7
                return $url;
446
            }
447 3
            return $url . $module;
448
        }
449
450 20
        $url .= $module . '/' . $controller;
451 20
        $getParams = [];
452 20
        foreach ($params as $key => $value) {
453
            // sub-array as GET params
454 15
            if (is_array($value)) {
455 1
                $getParams[$key] = $value;
456 1
                continue;
457
            }
458 14
            $url .= '/' . urlencode((string)$key) . '/' . urlencode((string)$value);
459
        }
460 20
        if (!empty($getParams)) {
461 1
            $url .= '?' . http_build_query($getParams);
462
        }
463 20
        return $url;
464
    }
465
466
    /**
467
     * Process routing
468
     *
469
     * @return void
470
     */
471 9
    public function process(): void
472
    {
473 9
        $this->processDefault() || // try to process default router (homepage)
474 5
        $this->processCustom() ||  //  or custom routers
475 5
        $this->processRoute();     //  or default router schema
476
477 9
        $this->resetRequest();
478 9
    }
479
480
    /**
481
     * Process default router
482
     *
483
     * @return bool
484
     */
485 9
    protected function processDefault(): bool
486
    {
487 9
        $uri = $this->getCleanUri();
488 9
        return empty($uri);
489
    }
490
491
    /**
492
     * Process custom router
493
     *
494
     * @return bool
495
     */
496 5
    protected function processCustom(): bool
497
    {
498 5
        $uri = '/' . $this->getCleanUri();
499 5
        foreach ($this->routers as $router) {
500 5
            if (preg_match($router['pattern'], $uri, $matches)) {
501
                $this->setParam('_module', $router['module']);
502
                $this->setParam('_controller', $router['controller']);
503
504
                foreach ($router['params'] as $param => $type) {
505
                    if (isset($matches[$param])) {
506
                        $this->setParam($param, $matches[$param]);
507
                    }
508
                }
509
                return true;
510
            }
511
        }
512 5
        return false;
513
    }
514
515
    /**
516
     * Process router by default rules
517
     *
518
     * Default routers examples
519
     *     /
520
     *     /:module/
521
     *     /:module/:controller/
522
     *     /:module/:controller/:key1/:value1/:key2/:value2...
523
     *
524
     * @return bool
525
     */
526 5
    protected function processRoute(): bool
527
    {
528 5
        $uri = $this->getCleanUri();
529 5
        $uri = trim($uri, '/');
530 5
        $raw = explode('/', $uri);
531
532
        // rewrite module from request
533 5
        if (count($raw)) {
534 5
            $this->setParam('_module', array_shift($raw));
535
        }
536
        // rewrite module from controller
537 5
        if (count($raw)) {
538 5
            $this->setParam('_controller', array_shift($raw));
539
        }
540 5
        if ($size = count($raw)) {
541
            // save raw
542
            $this->rawParams = $raw;
543
544
            // save as index params
545
            foreach ($raw as $i => $value) {
546
                $this->setParam($i, $value);
547
            }
548
549
            // remove tail
550
            if ($size % 2 === 1) {
551
                array_pop($raw);
552
                $size = count($raw);
553
            }
554
            // or use array_chunk and run another loop?
555
            for ($i = 0; $i < $size; $i += 2) {
556
                $this->setParam($raw[$i], $raw[$i + 1]);
557
            }
558
        }
559 5
        return true;
560
    }
561
562
    /**
563
     * Reset Request
564
     *
565
     * @return void
566
     */
567 9
    protected function resetRequest(): void
568
    {
569 9
        $request = Request::getInstance();
570
571
        // priority:
572
        //  - default values
573
        //  - from GET query
574
        //  - from path
575 9
        $request = $request->withQueryParams(
576 9
            array_merge(
577
                [
578 9
                    '_module' => $this->getDefaultModule(),
579 9
                    '_controller' => $this->getDefaultController()
580
                ],
581 9
                $request->getQueryParams(),
582 9
                $this->params
583
            )
584
        );
585 9
        Request::setInstance($request);
586 9
    }
587
}
588