Passed
Push — master ( 7070d1...90aa69 )
by Sebastian
03:34
created

Router   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 332
Duplicated Lines 0 %

Test Coverage

Coverage 99.03%

Importance

Changes 4
Bugs 2 Features 1
Metric Value
wmc 25
eloc 102
c 4
b 2
f 1
dl 0
loc 332
ccs 102
cts 103
cp 0.9903
rs 10

11 Methods

Rating   Name   Duplication   Size   Complexity  
A buildParam() 0 15 2
A validate() 0 12 2
A findRoute() 0 17 4
A __construct() 0 18 1
A buildParamFromQueryString() 0 29 4
A buildValidRoute() 0 24 3
A __call() 0 17 2
A map() 0 3 1
A getRoute() 0 3 1
A filterUri() 0 23 3
A buildErrorRoute() 0 11 2
1
<?php
2
3
/**
4
 * Linna Framework.
5
 *
6
 * @author Sebastian Rapetti <[email protected]>
7
 * @copyright (c) 2018, Sebastian Rapetti
8
 * @license http://opensource.org/licenses/MIT MIT License
9
 */
10
declare(strict_types=1);
11
12
namespace Linna\Router;
13
14
use BadMethodCallException;
15
16
/**
17
 * Router.
18
 *
19
 * Manage routes, verify every resource requested by browser and return
20
 * a RouteInterface Object.
21
 */
22
class Router
23
{
24
    /**
25
     * @var string Base path for route evaluation.
26
     */
27
    protected $basePath = '/';
28
29
    /**
30
     * @var bool|string Fallback route name for 404 errors.
31
     */
32
    protected $badRoute = false;
33
34
    /**
35
     * @var bool Use of url rewriting.
36
     */
37
    protected $rewriteMode = false;
38
39
    /**
40
     * @var string Router access point without rewrite engine.
41
     */
42
    protected $rewriteModeOffRouter = '/index.php?';
43
44
    /**
45
     * @var boor Parse query string when rewrite mode is on.
0 ignored issues
show
Bug introduced by
The type Linna\Router\boor was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
46
     */
47
    protected $parseQueryStringOnRewriteModeOn = false;
48
49
    /**
50
     * @var RouteInterface Utilized for return the most recently parsed route
51
     */
52
    protected $route;
53
54
    /**
55
     * @var RouteCollection Passed from constructor, is the list of registerd routes for the app
56
     */
57
    private $routes;
58
59
    /**
60
     * @var array List of regex for find parameter inside passed routes
61
     */
62
    private $matchTypes = [
63
        '`\[[0-9A-Za-z._-]+\]`',
64
    ];
65
66
    /**
67
     * @var array List of regex for find type of parameter inside passed routes
68
     */
69
    private $types = [
70
        '[0-9A-Za-z._-]++',
71
    ];
72
73
    /**
74
     * @var array preg_match result for route.
75
     */
76
    private $routeMatches = [];
77
78
    /**
79
     * @var array Array with parameters from query string.
80
     */
81
    private $queryParam = [];
82
83
    /**
84
     * Constructor.
85
     * Accept as parameter a RouteCollection object and an array options.
86
     *
87
     * @param RouteCollection $routes
88
     * @param array           $options
89
     */
90 19
    public function __construct(RouteCollection $routes, array $options = [])
91
    {
92
        [
93 19
            'basePath'             => $this->basePath,
94 19
            'badRoute'             => $this->badRoute,
95 19
            'rewriteMode'          => $this->rewriteMode,
96 19
            'rewriteModeOffRouter' => $this->rewriteModeOffRouter,
97 19
            'parseQueryStringOnRewriteModeOn' => $this->parseQueryStringOnRewriteModeOn
98 19
        ] = \array_replace_recursive([
99 19
            'basePath'             => $this->basePath,
100 19
            'badRoute'             => $this->badRoute,
101 19
            'rewriteMode'          => $this->rewriteMode,
102 19
            'rewriteModeOffRouter' => $this->rewriteModeOffRouter,
103 19
            'parseQueryStringOnRewriteModeOn' => $this->parseQueryStringOnRewriteModeOn
104 19
        ], $options);
105
106
        //set routes
107 19
        $this->routes = $routes;
108 19
    }
109
110
    /**
111
     * Evaluate request uri.
112
     *
113
     * @param string $requestUri
114
     * @param string $requestMethod
115
     *
116
     * @return bool
117
     */
118 60
    public function validate(string $requestUri, string $requestMethod): bool
119
    {
120 60
        $route = $this->findRoute($this->filterUri($requestUri), $requestMethod);
121
122 60
        if ($route instanceof Route) {
0 ignored issues
show
introduced by
$route is never a sub-type of Linna\Router\Route.
Loading history...
123 54
            $this->buildValidRoute($route);
124 54
            return true;
125
        }
126
127 6
        $this->buildErrorRoute();
128
129 6
        return false;
130
    }
131
132
    /**
133
     * Find if provided route match with one of registered routes.
134
     *
135
     * @param string $uri
136
     * @param string $method
137
     *
138
     * @return RouteInterface
139
     */
140 60
    private function findRoute(string $uri, string $method): RouteInterface
141
    {
142 60
        $matches = [];
143 60
        $route = new NullRoute();
144
145 60
        foreach ($this->routes as $value) {
146 60
            $urlMatch = \preg_match('`^'.\preg_replace($this->matchTypes, $this->types, $value->url).'/?$`', $uri, $matches);
147 60
            $methodMatch = \strpos($value->method, $method);
148
149 60
            if ($urlMatch && $methodMatch !== false) {
150 54
                $route = clone $value;
151 54
                $this->routeMatches = $matches;
152 60
                break;
153
            }
154
        }
155
156 60
        return $route;
157
    }
158
159
    /**
160
     * Build a valid route.
161
     *
162
     * @param Route $route
163
     *
164
     * @return void
165
     */
166 54
    private function buildValidRoute(Route $route): void
167
    {
168
        //add to route array the passed uri for param check when call
169 54
        $matches = $this->routeMatches;
170
171
        //route match and there is a subpattern with action
172 54
        if (\count($matches) > 1) {
173
            //assume that subpattern rapresent action
174 10
            $route->action = $matches[1];
175
176
            //url clean
177 10
            $route->url = \preg_replace('`\([0-9A-Za-z\|]++\)`', $matches[1], $route->url);
178
        }
179
180 54
        $queryParam = [];
181
182 54
        if ($this->parseQueryStringOnRewriteModeOn) {
183 5
            $queryParam = $this->queryParam;
184
        }
185
186
        //array union operator :)
187 54
        $route->param = $this->buildParam($route) + $queryParam;
188
189 54
        $this->route = $route;
190 54
    }
191
192
    /**
193
     * Try to find parameters in a valid route and return it.
194
     *
195
     * @param Route $route
196
     *
197
     * @return array
198
     */
199 54
    private function buildParam(Route $route): array
200
    {
201 54
        $param = [];
202
203 54
        $url = \explode('/', $route->url);
204 54
        $matches = \explode('/', $this->routeMatches[0]);
205
206 54
        $rawParam = \array_diff($matches, $url);
207
208 54
        foreach ($rawParam as $key => $value) {
209 28
            $paramName = \strtr($url[$key], ['[' => '', ']' => '']);
210 28
            $param[$paramName] = $value;
211
        }
212
213 54
        return $param;
214
    }
215
216
    /**
217
     * Create an array from query string params.
218
     *
219
     * @param string $queryString
220
     *
221
     * @return void
222
     */
223 5
    private function buildParamFromQueryString(string $queryString): void
224
    {
225
        $param = \array_map(function ($array_value) {
226 5
            $tmp = \explode('=', $array_value);
227 5
            $array_value = [];
228 5
            $key = $tmp[0];
229 5
            $value = '';
230
231 5
            if (isset($tmp[1])) {
232 5
                $value = \urldecode($tmp[1]);
233
            }
234
235 5
            $array_value[$key] = $value;
236
237 5
            return $array_value;
238 5
        }, \explode('&', $queryString));
239
240 5
        $temp = [];
241
242 5
        foreach ($param as $value) {
243 5
            if (\is_array($value)) {
244 5
                $temp = \array_merge($temp, $value);
245 5
                continue;
246
            }
247
248
            $temp[] = $value;
249
        }
250
251 5
        $this->queryParam = $temp;
252 5
    }
253
254
    /**
255
     * Actions for error route.
256
     *
257
     * @return void
258
     */
259 6
    private function buildErrorRoute(): void
260
    {
261
        //check if there is a declared route for errors, if no exit with false
262 6
        if (($key = \array_search($this->badRoute, \array_column($this->routes->getArrayCopy(), 'name'), true)) === false) {
263 2
            $this->route = new NullRoute();
264
265 2
            return;
266
        }
267
268
        //build and store route for errors
269 4
        $this->route = $this->routes[$key];
270 4
    }
271
272
    /**
273
     * Check if a route is valid and
274
     * return the route object else return a bad route object.
275
     *
276
     * @return RouteInterface
277
     */
278 60
    public function getRoute(): RouteInterface
279
    {
280 60
        return $this->route;
281
    }
282
283
    /**
284
     * Analize current uri, sanitize and return it.
285
     *
286
     * @param string $passedUri
287
     *
288
     * @return string
289
     */
290 60
    private function filterUri(string $passedUri): string
291
    {
292
        //sanitize url
293 60
        $url = \filter_var($passedUri, FILTER_SANITIZE_URL);
294
295
        //check for rewrite mode
296 60
        $url = \str_replace($this->rewriteModeOffRouter, '', $url);
297
298
        //remove basepath
299 60
        $url = \substr($url, \strlen($this->basePath));
300
301
        //remove doubled slash
302 60
        $url = \str_replace('//', '/', $url);
303
304
        //check for query string parameters
305 60
        if (\strpos($url, '?') !== false) {
306 5
            $queryString = \strstr($url, '?');
307 5
            $queryString = \substr($queryString, 1);
308 5
            $url = \strstr($url, '?', true);
309 5
            $this->buildParamFromQueryString($queryString);
310
        }
311
312 60
        return (\substr($url, 0, 1) === '/') ? $url : '/'.$url;
313
    }
314
315
    /**
316
     * Map a route.
317
     *
318
     * @param RouteInterface $route
319
     *
320
     * @return void
321
     */
322 15
    public function map(RouteInterface $route): void
323
    {
324 15
        $this->routes[] = $route;
325 15
    }
326
327
    /**
328
     * Fast route mapping.
329
     *
330
     * @param string $name
331
     * @param array  $arguments
332
     *
333
     * @return void
334
     *
335
     * @throws BadMethodCallException
336
     */
337 11
    public function __call(string $name, array $arguments)
338
    {
339 11
        $method = \strtoupper($name);
340
341 11
        if (\in_array($method, ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])) {
342 10
            $this->map(
343 10
                new Route(\array_merge($arguments[2] ?? [], [
344 10
                    'method'   => $method,
345 10
                    'url'      => $arguments[0],
346 10
                    'callback' => $arguments[1]
347
                ]))
348
            );
349
350 10
            return;
351
        }
352
353 1
        throw new BadMethodCallException("Router->{$name}() method do not exist.");
354
    }
355
}
356