Completed
Push — b0.25.0 ( 57b0c8...7e0be7 )
by Sebastian
04:25
created

Router::getRoute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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