Passed
Push — master ( 893914...a84b24 )
by Sebastian
04:40
created

Router::__call()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

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