Completed
Push — master ( 929e4f...acab5f )
by Anton
07:41
created

Router::getUrl()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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