Passed
Push — master ( 7f563c...6b4ab5 )
by Sebastian
05:32 queued 11s
created

Router::buildParamFromQueryString()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 13
c 1
b 0
f 1
dl 0
loc 25
ccs 0
cts 13
cp 0
rs 9.8333
cc 3
nc 3
nop 1
crap 12
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 14
    public function __construct(RouteCollection $routes, array $options = [])
91
    {
92
        [
93 14
            'basePath'             => $this->basePath,
94 14
            'badRoute'             => $this->badRoute,
95 14
            'rewriteMode'          => $this->rewriteMode,
96 14
            'rewriteModeOffRouter' => $this->rewriteModeOffRouter,
97 14
            'parseQueryStringOnRewriteModeOn' => $this->parseQueryStringOnRewriteModeOn
98 14
        ] = \array_replace_recursive([
99 14
            'basePath'             => $this->basePath,
100 14
            'badRoute'             => $this->badRoute,
101 14
            'rewriteMode'          => $this->rewriteMode,
102 14
            'rewriteModeOffRouter' => $this->rewriteModeOffRouter,
103 14
            'parseQueryStringOnRewriteModeOn' => $this->parseQueryStringOnRewriteModeOn
104 14
        ], $options);
105
106
        //set routes
107 14
        $this->routes = $routes;
108 14
    }
109
110
    /**
111
     * Evaluate request uri.
112
     *
113
     * @param string $requestUri
114
     * @param string $requestMethod
115
     *
116
     * @return bool
117
     */
118 55
    public function validate(string $requestUri, string $requestMethod): bool
119
    {
120 55
        $route = $this->findRoute($this->filterUri($requestUri), $requestMethod);
121
122 55
        if ($route instanceof Route) {
0 ignored issues
show
introduced by
$route is never a sub-type of Linna\Router\Route.
Loading history...
123 49
            $this->buildValidRoute($route);
124 49
            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 55
    private function findRoute(string $uri, string $method): RouteInterface
141
    {
142 55
        $matches = [];
143 55
        $route = new NullRoute();
144
145 55
        foreach ($this->routes as $value) {
146 55
            $urlMatch = \preg_match('`^'.\preg_replace($this->matchTypes, $this->types, $value->url).'/?$`', $uri, $matches);
147 55
            $methodMatch = \strpos($value->method, $method);
148
149 55
            if ($urlMatch && $methodMatch !== false) {
150 49
                $route = clone $value;
151 49
                $this->routeMatches = $matches;
152 55
                break;
153
            }
154
        }
155
156 55
        return $route;
157
    }
158
159
    /**
160
     * Build a valid route.
161
     *
162
     * @param Route $route
163
     *
164
     * @return void
165
     */
166 49
    private function buildValidRoute(Route $route): void
167
    {
168
        //add to route array the passed uri for param check when call
169 49
        $matches = $this->routeMatches;
170
171
        //route match and there is a subpattern with action
172 49
        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 49
        $queryParam = [];
181
182 49
        if ($this->parseQueryStringOnRewriteModeOn) {
183
            $queryParam = $this->queryParam;
184
        }
185
186
        //array union operator :)
187 49
        $route->param = $this->buildParam($route) + $queryParam;
188
189 49
        $this->route = $route;
190 49
    }
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 49
    private function buildParam(Route $route): array
200
    {
201 49
        $param = [];
202
203 49
        $url = \explode('/', $route->url);
204 49
        $matches = \explode('/', $this->routeMatches[0]);
205
206 49
        $rawParam = \array_diff($matches, $url);
207
208 49
        foreach ($rawParam as $key => $value) {
209 28
            $paramName = \strtr($url[$key], ['[' => '', ']' => '']);
210 28
            $param[$paramName] = $value;
211
        }
212
213 49
        return $param;
214
    }
215
216
    /**
217
     * Create an array from query string params.
218
     * 
219
     * @param string $queryString
220
     * 
221
     * @return void
222
     */
223
    private function buildParamFromQueryString(string $queryString): void
224
    {
225
        $param = \array_map(function($value){
226
227
            $tmp = \explode('=', $value);
228
            $value = [];
229
            $value[$tmp[0]] = urldecode($tmp[1]);
230
231
            return $value;
232
233
        }, \explode('&', $queryString));
234
        
235
        $temp = [];
236
237
        foreach ($param as $value) {
238
239
            if (\is_array($value)) {
240
                $temp = \array_merge($temp, $value);
241
                continue;
242
            }
243
244
            $temp[] = $value;
245
        }
246
247
        $this->queryParam = $temp;
248
    }
249
250
    /**
251
     * Actions for error route.
252
     *
253
     * @return void
254
     */
255 6
    private function buildErrorRoute(): void
256
    {
257
        //check if there is a declared route for errors, if no exit with false
258 6
        if (($key = \array_search($this->badRoute, \array_column($this->routes->getArrayCopy(), 'name'), true)) === false) {
259 2
            $this->route = new NullRoute();
260
261 2
            return;
262
        }
263
264
        //build and store route for errors
265 4
        $this->route = $this->routes[$key];
266 4
    }
267
268
    /**
269
     * Check if a route is valid and
270
     * return the route object else return a bad route object.
271
     *
272
     * @return RouteInterface
273
     */
274 55
    public function getRoute(): RouteInterface
275
    {
276 55
        return $this->route;
277
    }
278
279
    /**
280
     * Analize current uri, sanitize and return it.
281
     *
282
     * @param string $passedUri
283
     *
284
     * @return string
285
     */
286 55
    private function filterUri(string $passedUri): string
287
    {
288
        //sanitize url
289 55
        $url = \filter_var($passedUri, FILTER_SANITIZE_URL);
290
291
        //check for rewrite mode
292 55
        $url = \str_replace($this->rewriteModeOffRouter, '', $url);
293
294
        //remove basepath
295 55
        $url = \substr($url, \strlen($this->basePath));
296
297
        //remove doubled slash
298 55
        $url = \str_replace('//', '/', $url);
299
300
        //check for query string parameters
301 55
        if (strpos($url, '?') !== false) {
302
            $queryString = \strstr($url, '?');
303
            $queryString = \substr($queryString, 1);
304
            $url = \strstr($url, '?', true);
305
            $this->buildParamFromQueryString($queryString);
306
        }
307
308 55
        return (\substr($url, 0, 1) === '/') ? $url : '/'.$url;
309
    }
310
311
    /**
312
     * Map a route.
313
     *
314
     * @param RouteInterface $route
315
     *
316
     * @return void
317
     */
318 15
    public function map(RouteInterface $route): void
319
    {
320 15
        $this->routes[] = $route;
321 15
    }
322
323
    /**
324
     * Fast route mapping.
325
     *
326
     * @param string $name
327
     * @param array  $arguments
328
     *
329
     * @return void
330
     *
331
     * @throws BadMethodCallException
332
     */
333 11
    public function __call(string $name, array $arguments)
334
    {
335 11
        $method = \strtoupper($name);
336
337 11
        if (\in_array($method, ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])) {
338 10
            $this->map(
339 10
                new Route(\array_merge($arguments[2] ?? [], [
340 10
                    'method'   => $method,
341 10
                    'url'      => $arguments[0],
342 10
                    'callback' => $arguments[1]
343
                ]))
344
            );
345
346 10
            return;
347
        }
348
349 1
        throw new BadMethodCallException("Router->{$name}() method do not exist.");
350
    }
351
}
352