Completed
Pull Request — master (#413)
by Anton
06:02
created

Router::processCustom()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 9.0581

Importance

Changes 0
Metric Value
cc 5
eloc 11
nc 5
nop 0
dl 0
loc 18
ccs 5
cts 11
cp 0.4545
crap 9.0581
rs 8.8571
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($module = self::DEFAULT_MODULE, $controller = self::DEFAULT_CONTROLLER, array $params = [])
319
    {
320 34
        $module = $module ?? Request::getModule();
321 34
        $controller = $controller ?? Request::getController();
322
323 34
        if (isset($this->reverse[$module], $this->reverse[$module][$controller])) {
324 5
            return $this->urlCustom($module, $controller, $params);
325
        }
326
327 29
        return $this->urlRoute($module, $controller, $params);
328
    }
329
330
    /**
331
     * Build full URL to controller
332
     *
333
     * @param  string $module
334
     * @param  string $controller
335
     * @param  array  $params
336
     * @return string
337
     */
338 1
    public function getFullUrl(
339
        $module = self::DEFAULT_MODULE,
340
        $controller = self::DEFAULT_CONTROLLER,
341
        array $params = []
342
    ) {
343 1
        $scheme = Request::getInstance()->getUri()->getScheme() . '://';
344 1
        $host = Request::getInstance()->getUri()->getHost();
345 1
        $url = $this->getUrl($module, $controller, $params);
346 1
        return $scheme . $host . $url;
347
    }
348
349
    /**
350
     * Build URL by custom route
351
     *
352
     * @param  string $module
353
     * @param  string $controller
354
     * @param  array  $params
355
     * @return string
356
     */
357 5
    protected function urlCustom($module, $controller, $params)
358
    {
359 5
        $url = $this->reverse[$module][$controller]['route'];
360
361 5
        $getParams = [];
362 5
        foreach ($params as $key => $value) {
363
            // sub-array as GET params
364 4
            if (is_array($value)) {
365 1
                $getParams[$key] = $value;
366 1
                continue;
367
            }
368 3
            $url = str_replace('{$' . $key . '}', $value, $url, $replaced);
369
            // if not replaced, setup param as GET
370 3
            if (!$replaced) {
371
                $getParams[$key] = $value;
372
            }
373
        }
374
        // clean optional params
375 5
        $url = preg_replace('/\{\$[a-z0-9-_]+\}/i', '', $url);
376
        // clean regular expression (.*)
377 5
        $url = preg_replace('/\(\.\*\)/', '', $url);
378
        // replace "//" with "/"
379 5
        $url = str_replace('//', '/', $url);
380
381 5
        if (!empty($getParams)) {
382 1
            $url .= '?' . http_build_query($getParams);
383
        }
384 5
        return $this->getBaseUrl() . ltrim($url, '/');
385
    }
386
387
    /**
388
     * Build URL by default route
389
     *
390
     * @param  string $module
391
     * @param  string $controller
392
     * @param  array  $params
393
     * @return string
394
     */
395 29
    protected function urlRoute($module, $controller, $params)
396
    {
397 29
        $url = $this->getBaseUrl();
398
399 29
        if (empty($params)) {
400 15
            if ($controller === self::DEFAULT_CONTROLLER) {
401 10
                if ($module === self::DEFAULT_MODULE) {
402 7
                    return $url;
403
                }
404 3
                return $url . $module;
405
            }
406
        }
407
408 20
        $url .= $module . '/' . $controller;
409 20
        $getParams = [];
410 20
        foreach ($params as $key => $value) {
411
            // sub-array as GET params
412 15
            if (is_array($value)) {
413 1
                $getParams[$key] = $value;
414 1
                continue;
415
            }
416 14
            $url .= '/' . urlencode((string)$key) . '/' . urlencode((string)$value);
417
        }
418 20
        if (!empty($getParams)) {
419 1
            $url .= '?' . http_build_query($getParams);
420
        }
421 20
        return $url;
422
    }
423
424
    /**
425
     * Process routing
426
     *
427
     * @return \Bluz\Router\Router
428
     */
429 6
    public function process()
430
    {
431
        switch (true) {
432
            // try process default router
433 6
            case $this->processDefault():
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
434 1
                break;
435
            // try process custom routers
436 5
            case $this->processCustom():
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
437
                break;
438
            // try process router
439 5
            case $this->processRoute():
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
440 5
                break;
441
        }
442
443 6
        $this->resetRequest();
444 6
        return $this;
445
    }
446
447
    /**
448
     * Process default router
449
     *
450
     * @return bool
451
     */
452 6
    protected function processDefault()
453
    {
454 6
        $uri = $this->getCleanUri();
455 6
        return empty($uri);
456
    }
457
458
    /**
459
     * Process custom router
460
     *
461
     * @return bool
462
     */
463 5
    protected function processCustom()
464
    {
465 5
        $uri = '/' . $this->getCleanUri();
466 5
        foreach ($this->routers as $router) {
467 5
            if (preg_match($router['pattern'], $uri, $matches)) {
468
                $this->setParam('_module', $router['module']);
469
                $this->setParam('_controller', $router['controller']);
470
471
                foreach ($router['params'] as $param => $type) {
472
                    if (isset($matches[$param])) {
473
                        $this->setParam($param, $matches[$param]);
474
                    }
475
                }
476
                return true;
477
            }
478
        }
479 5
        return false;
480
    }
481
482
    /**
483
     * Process router by default rules
484
     *
485
     * Default routers examples
486
     *     /
487
     *     /:module/
488
     *     /:module/:controller/
489
     *     /:module/:controller/:key1/:value1/:key2/:value2...
490
     *
491
     * @return bool
492
     */
493 5
    protected function processRoute()
494
    {
495 5
        $uri = $this->getCleanUri();
496 5
        $uri = trim($uri, '/');
497 5
        $raw = explode('/', $uri);
498
499
        // rewrite module from request
500 5
        if (count($raw)) {
501 5
            $this->setParam('_module', array_shift($raw));
502
        }
503
        // rewrite module from controller
504 5
        if (count($raw)) {
505 5
            $this->setParam('_controller', array_shift($raw));
506
        }
507 5
        if ($size = count($raw)) {
508
            // save raw
509
            $this->rawParams = $raw;
510
511
            // save as index params
512
            foreach ($raw as $i => $value) {
513
                $this->setParam($i, $value);
514
            }
515
516
            // remove tail
517
            if ($size % 2 == 1) {
518
                array_pop($raw);
519
                $size = count($raw);
520
            }
521
            // or use array_chunk and run another loop?
522
            for ($i = 0; $i < $size; $i += 2) {
523
                $this->setParam($raw[$i], $raw[$i + 1]);
524
            }
525
        }
526 5
        return true;
527
    }
528
529
    /**
530
     * Reset Request
531
     *
532
     * @return void
533
     */
534 6
    protected function resetRequest()
535
    {
536 6
        $request = Request::getInstance();
537
538
        // priority:
539
        //  - default values
540
        //  - from GET query
541
        //  - from path
542 6
        $request = $request->withQueryParams(
543
            array_merge(
544
                [
545 6
                    '_module' => $this->getDefaultModule(),
546 6
                    '_controller' => $this->getDefaultController()
547
                ],
548 6
                $request->getQueryParams(),
549 6
                $this->params
550
            )
551
        );
552 6
        Request::setInstance($request);
553 6
    }
554
555
    /**
556
     * Get the request URI without baseUrl
557
     *
558
     * @return string
559
     */
560 17
    public function getCleanUri()
561
    {
562 17
        if ($this->cleanUri === null) {
563 17
            $uri = Request::getUri()->getPath();
564 17
            if ($this->getBaseUrl() && strpos($uri, $this->getBaseUrl()) === 0) {
565 17
                $uri = substr($uri, strlen($this->getBaseUrl()));
566
            }
567 17
            $this->cleanUri = $uri;
568
        }
569 17
        return $this->cleanUri;
570
    }
571
}
572