Completed
Pull Request — master (#470)
by Anton
14:03 queued 11:44
created

Router::resetRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

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