Completed
Push — master ( 8c3e1f...44bcc9 )
by Sebastian
05:55
created

Router   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 267
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 19
eloc 73
dl 0
loc 267
ccs 72
cts 72
cp 1
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 16 1
A buildParam() 0 15 2
A buildValidRoute() 0 17 2
A __call() 0 17 2
A map() 0 3 1
A getRoute() 0 3 1
A filterUri() 0 15 2
A validate() 0 12 2
A buildErrorRoute() 0 11 2
A findRoute() 0 17 4
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 RouteInterface Utilized for return the most recently parsed route
46
     */
47
    protected $route;
48
49
    /**
50
     * @var array 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
     * Constructor.
75
     * Accept as parameter a list routes and options.
76
     *
77
     * @param array $routes
78
     * @param array $options
79
     */
80 14
    public function __construct(array $routes, array $options = [])
81
    {
82
        [
83 14
            'basePath'             => $this->basePath,
84 14
            'badRoute'             => $this->badRoute,
85 14
            'rewriteMode'          => $this->rewriteMode,
86 14
            'rewriteModeOffRouter' => $this->rewriteModeOffRouter
87 14
        ] = array_replace_recursive([
88 14
            'basePath'             => '/',
89
            'badRoute'             => false,
90
            'rewriteMode'          => false,
91
            'rewriteModeOffRouter' => '/index.php?'
92 14
        ], $options);
93
94
        //set routes
95 14
        $this->routes = $routes;
96 14
    }
97
98
    /**
99
     * Evaluate request uri.
100
     *
101
     * @param string $requestUri
102
     * @param string $requestMethod
103
     *
104
     * @return bool
105
     */
106 49
    public function validate(string $requestUri, string $requestMethod): bool
107
    {
108 49
        $route = $this->findRoute($this->filterUri($requestUri), $requestMethod);
109
110 49
        if ($route instanceof Route) {
0 ignored issues
show
introduced by
$route is never a sub-type of Linna\Router\Route.
Loading history...
111 43
            $this->buildValidRoute($route);
112 43
            return true;
113
        }
114
115 6
        $this->buildErrorRoute();
116
117 6
        return false;
118
    }
119
120
    /**
121
     * Find if provided route match with one of registered routes.
122
     *
123
     * @param string $uri
124
     * @param string $method
125
     *
126
     * @return RouteInterface
127
     */
128 49
    private function findRoute(string $uri, string $method): RouteInterface
129
    {
130 49
        $matches = [];
131 49
        $route = new NullRoute();
132
133 49
        foreach ($this->routes as $value) {
134 49
            $urlMatch = preg_match('`^'.preg_replace($this->matchTypes, $this->types, $value->url).'/?$`', $uri, $matches);
135 49
            $methodMatch = strpos($value->method, $method);
136
137 49
            if ($urlMatch && $methodMatch !== false) {
138 43
                $route = clone $value;
139 43
                $this->routeMatches = $matches;
140 49
                break;
141
            }
142
        }
143
144 49
        return $route;
145
    }
146
147
    /**
148
     * Build a valid route.
149
     *
150
     * @param Route $route
151
     *
152
     * @return void
153
     */
154 43
    private function buildValidRoute(Route $route): void
155
    {
156
        //add to route array the passed uri for param check when call
157 43
        $matches = $this->routeMatches;
158
159
        //route match and there is a subpattern with action
160 43
        if (count($matches) > 1) {
161
            //assume that subpattern rapresent action
162 10
            $route->action = $matches[1];
163
164
            //url clean
165 10
            $route->url = preg_replace('`\([0-9A-Za-z\|]++\)`', $matches[1], $route->url);
166
        }
167
168 43
        $route->param = $this->buildParam($route);
169
170 43
        $this->route = $route;
171 43
    }
172
173
    /**
174
     * Try to find parameters in a valid route and return it.
175
     *
176
     * @param Route $route
177
     *
178
     * @return array
179
     */
180 43
    private function buildParam(Route $route): array
181
    {
182 43
        $param = [];
183
184 43
        $url = explode('/', $route->url);
185 43
        $matches = explode('/', $this->routeMatches[0]);
186
187 43
        $rawParam = array_diff($matches, $url);
188
189 43
        foreach ($rawParam as $key => $value) {
190 22
            $paramName = strtr($url[$key], ['[' => '', ']' => '']);
191 22
            $param[$paramName] = $value;
192
        }
193
194 43
        return $param;
195
    }
196
197
    /**
198
     * Actions for error route.
199
     *
200
     * @return void
201
     */
202 6
    private function buildErrorRoute(): void
203
    {
204
        //check if there is a declared route for errors, if no exit with false
205 6
        if (($key = array_search($this->badRoute, array_column($this->routes, 'name'), true)) === false) {
206 2
            $this->route = new NullRoute();
207
208 2
            return;
209
        }
210
211
        //build and store route for errors
212 4
        $this->route = $this->routes[$key];
213 4
    }
214
215
    /**
216
     * Check if a route is valid and
217
     * return the route object else return a bad route object.
218
     *
219
     * @return RouteInterface
220
     */
221 49
    public function getRoute(): RouteInterface
222
    {
223 49
        return $this->route;
224
    }
225
226
    /**
227
     * Analize $_SERVER['REQUEST_URI'] for current uri, sanitize and return it.
228
     *
229
     * @param string $passedUri
230
     *
231
     * @return string
232
     */
233 49
    private function filterUri(string $passedUri): string
234
    {
235
        //sanitize url
236 49
        $url = filter_var($passedUri, FILTER_SANITIZE_URL);
237
238
        //check for rewrite mode
239 49
        $url = str_replace($this->rewriteModeOffRouter, '', $url);
240
241
        //remove basepath
242 49
        $url = substr($url, strlen($this->basePath));
243
244
        //remove doubled slash
245 49
        $url = str_replace('//', '/', $url);
246
247 49
        return (substr($url, 0, 1) === '/') ? $url : '/'.$url;
248
    }
249
250
    /**
251
     * Map a route.
252
     *
253
     * @param RouteInterface $route
254
     *
255
     * @return void
256
     */
257 15
    public function map(RouteInterface $route): void
258
    {
259 15
        array_push($this->routes, $route);
260 15
    }
261
262
    /**
263
     * Fast route mapping.
264
     *
265
     * @param string $name
266
     * @param array  $arguments
267
     *
268
     * @return void
269
     *
270
     * @throws BadMethodCallException
271
     */
272 11
    public function __call(string $name, array $arguments)
273
    {
274 11
        $method = strtoupper($name);
275
276 11
        if (in_array($method, ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])) {
277 10
            $this->map(
278 10
                new Route(array_merge($arguments[2] ?? [], [
279 10
                    'method'   => $method,
280 10
                    'url'      => $arguments[0],
281 10
                    'callback' => $arguments[1]
282
                ]))
283
            );
284
285 10
            return;
286
        }
287
288 1
        throw new BadMethodCallException("Router->{$name}() method do not exist.");
289
    }
290
}
291