Completed
Pull Request — master (#391)
by Anton
06:11
created

Router::processCustom()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 7.3471

Importance

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