Completed
Push — master ( e45033...b3fad6 )
by Arman
25s queued 13s
created

Router   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 391
Duplicated Lines 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
eloc 124
c 6
b 1
f 0
dl 0
loc 391
rs 5.5199
wmc 56

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getParamName() 0 13 4
A findRoute() 0 36 5
A checkCollision() 0 11 5
A handleNotFound() 0 6 2
A getRoutes() 0 3 1
A escape() 0 3 1
A resetRoutes() 0 4 1
A findPatternMatches() 0 14 3
A routeParams() 0 12 3
A handleRoutePattern() 0 34 4
B getParamPattern() 0 31 11
A checkSegment() 0 9 3
A checkParamName() 0 5 3
A setRoutes() 0 3 1
A checkMethod() 0 8 4
A normalizePattern() 0 11 4
A __construct() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Router often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Router, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Quantum PHP Framework
5
 *
6
 * An open source software development framework for PHP
7
 *
8
 * @package Quantum
9
 * @author Arman Ag. <[email protected]>
10
 * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
11
 * @link http://quantum.softberg.org/
12
 * @since 2.9.0
13
 */
14
15
namespace Quantum\Router;
16
17
use Quantum\Exceptions\StopExecutionException;
18
use Quantum\Exceptions\RouteException;
19
use Quantum\Exceptions\ViewException;
20
use Quantum\Exceptions\DiException;
21
use Quantum\Debugger\Debugger;
22
use Twig\Error\RuntimeError;
23
use Twig\Error\SyntaxError;
24
use Twig\Error\LoaderError;
25
use Quantum\Http\Response;
26
use Quantum\Http\Request;
27
use ReflectionException;
28
use Psr\Log\LogLevel;
29
30
/**
31
 * Class Router
32
 * @package Quantum\Router
33
 */
34
class Router extends RouteController
35
{
36
37
    /**
38
     * Parameter types
39
     */
40
    const PARAM_TYPES = [
41
        ':alpha' => '[a-zA-Z]',
42
        ':num' => '[0-9]',
43
        ':any' => '[^\/]'
44
    ];
45
46
    /**
47
     * Request instance
48
     * @var Request;
49
     */
50
    private $request;
51
52
    /**
53
     * Response instance
54
     * @var Response;
55
     */
56
    private $response;
57
58
    /**
59
     * List of routes
60
     * @var array
61
     */
62
    private static $routes = [];
63
64
    /**
65
     * matched routes
66
     * @var array
67
     */
68
    private $matchedRoutes = [];
69
70
    /**
71
     * Router constructor.
72
     * @param Request $request
73
     * @param Response $response
74
     */
75
    public function __construct(Request $request, Response $response)
76
    {
77
        $this->request = $request;
78
        $this->response = $response;
79
    }
80
81
    /**
82
     * Finds the current route
83
     * @throws StopExecutionException
84
     * @throws ViewException
85
     * @throws DiException
86
     * @throws LoaderError
87
     * @throws RuntimeError
88
     * @throws SyntaxError
89
     * @throws RouteException
90
     * @throws ReflectionException
91
     */
92
    public function findRoute()
93
    {
94
        $uri = $this->request->getUri();
95
96
        if (!$uri) {
97
            throw RouteException::notFound();
98
        }
99
100
        $this->findPatternMatches($uri);
101
102
        if (!count($this->matchedRoutes)) {
103
            stop(function () {
104
                $this->handleNotFound();
105
            });
106
        }
107
108
        if (count($this->matchedRoutes) > 1) {
109
            $this->checkCollision();
110
        }
111
112
        $matchedRoute = current($this->matchedRoutes);
113
114
        $this->checkMethod($matchedRoute);
115
116
        $matchedRoute['uri'] = $uri;
117
118
        self::setCurrentRoute($matchedRoute);
119
120
        if (filter_var(config()->get(Debugger::DEBUG_ENABLED), FILTER_VALIDATE_BOOLEAN)) {
121
            $routeInfo = [];
122
123
            array_walk($matchedRoute, function ($value, $key) use (&$routeInfo) {
124
                $routeInfo[ucfirst($key)] = json_encode($value);
125
            });
126
127
            Debugger::addToStore(Debugger::ROUTES, LogLevel::INFO, $routeInfo);
128
        }
129
    }
130
131
    /**
132
     * Set Routes
133
     * @param array $routes
134
     */
135
    public static function setRoutes(array $routes)
136
    {
137
        self::$routes = $routes;
138
    }
139
140
    /**
141
     * Get Routes
142
     * @return array
143
     */
144
    public static function getRoutes(): array
145
    {
146
        return self::$routes;
147
    }
148
149
    /**
150
     * Resets the routes
151
     */
152
    public function resetRoutes()
153
    {
154
        parent::$currentRoute = null;
155
        $this->matchedRoutes = [];
156
    }
157
158
    /**
159
     * Finds matches by pattern
160
     * @param string $uri
161
     * @throws RouteException
162
     */
163
    private function findPatternMatches(string $uri)
164
    {
165
        $requestUri = urldecode(parse_url($uri)['path']);
166
167
        foreach (self::$routes as $route) {
168
169
            list($pattern, $params) = $this->handleRoutePattern($route);
170
171
            preg_match("/^" . $this->escape($pattern) . "$/u", $requestUri, $matches);
172
173
            if (count($matches)) {
174
                $route['params'] = $this->routeParams($params, $matches);
175
                $route['pattern'] = $pattern;
176
                $this->matchedRoutes[] = $route;
177
            }
178
        }
179
    }
180
181
    /**
182
     * Handles the route pattern
183
     * @param array $route
184
     * @return array
185
     * @throws RouteException
186
     */
187
    private function handleRoutePattern(array $route): array
188
    {
189
        $routeSegments = explode('/', trim($route['route'], '/'));
190
191
        $routePattern = '(\/)?';
192
        $routeParams = [];
193
194
        $lastIndex = (int)array_key_last($routeSegments);
195
196
        foreach ($routeSegments as $index => $segment) {
197
            $segmentParam = $this->checkSegment($segment, $index, $lastIndex);
198
199
            if (!empty($segmentParam)) {
200
                $this->checkParamName($routeParams, $segmentParam['name']);
201
202
                $routeParams[] = [
203
                    'route_pattern' => $segment,
204
                    'pattern' => $segmentParam['pattern'],
205
                    'name' => $segmentParam['name']
206
                ];
207
208
                $routePattern = $this->normalizePattern($routePattern, $segmentParam, $index, $lastIndex);
209
            } else {
210
                $routePattern .= $segment;
211
212
                if ($index != $lastIndex) {
213
                    $routePattern .= '(\/)';
214
                }
215
            }
216
        }
217
218
        return [
219
            $routePattern,
220
            $routeParams
221
        ];
222
    }
223
224
    /**
225
     * Normalize the pattern
226
     * @param string $routePattern
227
     * @param array $segmentParam
228
     * @param int $index
229
     * @param int $lastIndex
230
     * @return string
231
     */
232
    private function normalizePattern(string $routePattern, array $segmentParam, int $index, int $lastIndex): string
233
    {
234
        if ($index == $lastIndex) {
235
            if (mb_substr($routePattern, -5) == '(\/)?') {
236
                $routePattern = mb_substr($routePattern, 0, mb_strlen($routePattern) - 5);
237
            } elseif (mb_substr($routePattern, -4) == '(\/)') {
238
                $routePattern = mb_substr($routePattern, 0, mb_strlen($routePattern) - 4);
239
            }
240
        }
241
242
        return $routePattern .= $segmentParam['pattern'];
243
    }
244
245
    /**
246
     * Gets the route parameters
247
     * @param array $params
248
     * @param array $arguments
249
     * @return array
250
     */
251
    private function routeParams(array $params, array $arguments): array
252
    {
253
        $arguments = array_diff($arguments, ['', '/']);
254
255
        foreach ($params as &$param) {
256
            $param['value'] = $arguments[$param['name']] ?? null;
257
            if (mb_substr($param['name'], 0, 1) == '_') {
258
                $param['name'] = null;
259
            }
260
        }
261
262
        return $params;
263
    }
264
265
    /**
266
     * Checks the segment for parameter
267
     * @param string $segment
268
     * @param int $index
269
     * @param int $lastIndex
270
     * @return array
271
     * @throws RouteException
272
     */
273
    private function checkSegment(string $segment, int $index, int $lastIndex): array
274
    {
275
        foreach (self::PARAM_TYPES as $type => $expr) {
276
            if (preg_match('/\[(.*=)*(' . $type . ')(:([0-9]+))*\](\?)?/', $segment, $match)) {
277
                return $this->getParamPattern($match, $expr, $index, $lastIndex);
278
            }
279
        }
280
281
        return [];
282
    }
283
284
    /**
285
     * Checks the parameter name availability
286
     * @param array $routeParams
287
     * @param string $name
288
     * @throws RouteException
289
     */
290
    private function checkParamName(array $routeParams, string $name)
291
    {
292
        foreach ($routeParams as $param) {
293
            if ($param['name'] == $name) {
294
                throw RouteException::paramNameNotAvailable($name);
295
            }
296
        }
297
    }
298
299
    /**
300
     * Finds pattern for parameter
301
     * @param array $match
302
     * @param string $expr
303
     * @param int $index
304
     * @param int $lastIndex
305
     * @return array
306
     * @throws RouteException
307
     */
308
    private function getParamPattern(array $match, string $expr, int $index, int $lastIndex): array
309
    {
310
        $pattern = '';
311
312
        $name = $this->getParamName($match, $index);
313
314
        $pattern .= '(?<' . $name . '>' . $expr;
315
316
        if (isset($match[4]) && is_numeric($match[4])) {
317
            if (isset($match[5]) && $match[5] == '?') {
318
                $pattern .= '{0,' . $match[4] . '})';
319
            } else {
320
                $pattern .= '{' . $match[4] . '})';
321
            }
322
        } else {
323
            if (isset($match[5]) && $match[5] == '?') {
324
                $pattern .= '*)';
325
            } else {
326
                $pattern .= '+)';
327
            }
328
        }
329
330
        if (isset($match[5]) && $match[5] == '?') {
331
            $pattern = ($index == $lastIndex ? '(\/)?' . $pattern : $pattern . '(\/)?');
332
        } else {
333
            $pattern = ($index == $lastIndex ? '(\/)' . $pattern : $pattern . '(\/)');
334
        }
335
336
        return [
337
            'name' => $name,
338
            'pattern' => $pattern
339
        ];
340
    }
341
342
    /**
343
     * Gets the parameter name
344
     * @param array $match
345
     * @param int $index
346
     * @return string
347
     * @throws RouteException
348
     */
349
    private function getParamName(array $match, int $index): string
350
    {
351
        $name = $match[1] ? rtrim($match[1], '=') : null;
352
353
        if ($name) {
354
            if (!preg_match('/^[a-zA-Z]+$/', $name)) {
355
                throw RouteException::paramNameNotValid();
356
            }
357
        } else {
358
            $name = '_segment' . $index;
359
        }
360
361
        return $name;
362
    }
363
364
    /**
365
     * Checks the route collisions
366
     * @throws RouteException
367
     */
368
    private function checkCollision()
369
    {
370
        $length = count($this->matchedRoutes);
371
372
        for ($i = 0; $i < $length - 1; $i++) {
373
            for ($j = $i + 1; $j < $length; $j++) {
374
                if ($this->matchedRoutes[$i]['method'] == $this->matchedRoutes[$j]['method']) {
375
                    throw RouteException::repetitiveRouteSameMethod($this->matchedRoutes[$j]['method']);
376
                }
377
                if ($this->matchedRoutes[$i]['module'] != $this->matchedRoutes[$j]['module']) {
378
                    throw RouteException::repetitiveRouteDifferentModules();
379
                }
380
            }
381
        }
382
    }
383
384
    /**
385
     * Checks the request method against defined route method
386
     * @param array $matchedRoute
387
     * @throws RouteException
388
     */
389
    private function checkMethod(array $matchedRoute)
390
    {
391
        if (strpos($matchedRoute['method'], '|') !== false) {
392
            if (!in_array($this->request->getMethod(), explode('|', $matchedRoute['method']))) {
393
                throw RouteException::incorrectMethod($this->request->getMethod());
394
            }
395
        } else if ($this->request->getMethod() != $matchedRoute['method']) {
396
            throw RouteException::incorrectMethod($this->request->getMethod());
397
        }
398
    }
399
400
    /**
401
     * Escapes the slashes
402
     * @param string $str
403
     * @return string
404
     */
405
    private function escape(string $str): string
406
    {
407
        return str_replace('/', '\/', stripslashes($str));
408
    }
409
410
    /**
411
     * Handles page not found
412
     * @throws DiException
413
     * @throws ViewException
414
     * @throws ReflectionException
415
     * @throws LoaderError
416
     * @throws RuntimeError
417
     * @throws SyntaxError
418
     */
419
    private function handleNotFound()
420
    {
421
        if ($this->request->getHeader('Accept') == 'application/json') {
422
            $this->response->json(['status' => 'error', 'message' => 'Page not found'], 404);
423
        } else {
424
            $this->response->html(partial('errors/404'), 404);
425
        }
426
    }
427
428
}
429