Completed
Pull Request — master (#437)
by Anton
04:37
created

Router::getBaseUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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