Passed
Push — master ( 4af4e3...150718 )
by Sebastian
02:22
created

Router::createRouteArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 8
cts 8
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 8
nc 1
nop 4
crap 1
1
<?php
2
3
/**
4
 * Linna Framework.
5
 *
6
 * @author Sebastian Rapetti <[email protected]>
7
 * @copyright (c) 2017, Sebastian Rapetti
8
 * @license http://opensource.org/licenses/MIT MIT License
9
 */
10
declare(strict_types=1);
11
12
namespace Linna\Http;
13
14
use BadMethodCallException;
15
use Linna\Shared\ClassOptionsTrait;
16
17
/**
18
 * Router.
19
 *
20
 * Manage routes, verify every resource requested by browser and return
21
 * a RouteInterface Object.
22
 */
23
class Router
24
{
25
    use ClassOptionsTrait;
26
27
    /**
28
     * @var array Config options for class
29
     */
30
    protected $options = [
31
        'basePath'             => '/',
32
        'badRoute'             => false,
33
        'rewriteMode'          => false,
34
        'rewriteModeOffRouter' => '/index.php?',
35
    ];
36
37
    /**
38
     * @var RouteInterface Utilized for return the most recently parsed route
39
     */
40
    protected $route;
41
42
    /**
43
     * @var array Passed from constructor, is the list of registerd routes for the app
44
     */
45
    private $routes;
46
47
    /**
48
     * @var array List of regex for find parameter inside passed routes
49
     */
50
    private $matchTypes = [
51
        '`\[[0-9A-Za-z]+\]`',
52
    ];
53
54
    /**
55
     * @var array List of regex for find type of parameter inside passed routes
56
     */
57
    private $types = [
58
        '[0-9A-Za-z]++',
59
    ];
60
61
    /**
62
     * Constructor.
63
     * Accept as parameter a list routes and options.
64
     *
65
     * @param array $routes
66
     * @param array $options
67
     */
68 90
    public function __construct(array $routes = [], array $options = [])
69
    {
70
        //set options
71 90
        $this->setOptions($options);
72
73
        //set routes
74 90
        $this->routes = $routes;
75 90
    }
76
77
    /**
78
     * Evaluate request uri.
79
     *
80
     * @param string $requestUri
81
     * @param string $requestMethod
82
     *
83
     * @return bool
84
     */
85 66
    public function validate(string $requestUri, string $requestMethod) : bool
86
    {
87 66
        $route = $this->findRoute($this->filterUri($requestUri), $requestMethod);
88
89 66
        if ($route) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $route of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
90 55
            $this->buildValidRoute($route);
91
92 55
            return true;
93
        }
94
95 12
        $this->buildErrorRoute();
96
97 12
        return false;
98
    }
99
100
    /**
101
     * Find if provided route match with one of registered routes.
102
     *
103
     * @param string $uri
104
     * @param string $method
105
     *
106
     * @return array
107
     */
108 66
    private function findRoute(string $uri, string $method) : array
109
    {
110 66
        $matches = [];
111 66
        $route = [];
112
113 66
        foreach ($this->routes as $value) {
114 66
            $urlMatch = preg_match('`^'.preg_replace($this->matchTypes, $this->types, $value['url']).'/?$`', $uri, $matches);
115 66
            $methodMatch = strpos($value['method'], $method);
116
117 66
            if ($urlMatch && $methodMatch !== false) {
118 55
                $route = $value;
119 55
                $route['matches'] = $matches;
120 55
                break;
121
            }
122
        }
123
124 66
        return $route;
125
    }
126
127
    /**
128
     * Build a valid route.
129
     *
130
     * @param array $route
131
     */
132 55
    private function buildValidRoute(array $route)
133
    {
134
        //add to route array the passed uri for param check when call
135 55
        $matches = $route['matches'];
136
137
        //route match and there is a subpattern with action
138 55
        if (count($matches) > 1) {
139
            //assume that subpattern rapresent action
140 17
            $route['action'] = $matches[1];
141
142
            //url clean
143 17
            $route['url'] = preg_replace('`\([0-9A-Za-z\|]++\)`', $matches[1], $route['url']);
144
        }
145
146 55
        $route['param'] = $this->buildParam($route);
147
148
        //delete matches key because not required inside route object
149 55
        unset($route['matches']);
150
151 55
        $this->route = new Route($route);
152 55
    }
153
154
    /**
155
     * Try to find parameters in a valid route and return it.
156
     *
157
     * @param array $route
158
     *
159
     * @return array
160
     */
161 55
    private function buildParam(array $route): array
162
    {
163 55
        $param = [];
164
165 55
        $url = explode('/', $route['url']);
166 55
        $matches = explode('/', $route['matches'][0]);
167
168 55
        $rawParam = array_diff($matches, $url);
169
170 55
        foreach ($rawParam as $key => $value) {
171 24
            $paramName = strtr($url[$key], ['[' => '', ']' => '']);
172 24
            $param[$paramName] = $value;
173
        }
174
175 55
        return $param;
176
    }
177
178
    /**
179
     * Actions for error route.
180
     *
181
     * @return void
182
     */
183 12
    private function buildErrorRoute()
184
    {
185
        //check if there is a declared route for errors, if no exit with false
186 12
        if (($key = array_search($this->options['badRoute'], array_column($this->routes, 'name'), true)) === false) {
187 3
            $this->route = new NullRoute();
188
189 3
            return;
190
        }
191
192
        //pick route for errors
193 9
        $route = $this->routes[$key];
194
195
        //build and store route for errors
196 9
        $this->route = new Route($route);
197 9
    }
198
    
199
    /**
200
     * Check if a route is valid and
201
     * return the route object else return a bad route object.
202
     *
203
     * @return RouteInterface
204
     */
205 61
    public function getRoute() : RouteInterface
206
    {
207 61
        return $this->route;
208
    }
209
    
210
    /**
211
     * Analize $_SERVER['REQUEST_URI'] for current uri, sanitize and return it.
212
     *
213
     * @param string $passedUri
214
     *
215
     * @return string
216
     */
217 66
    private function filterUri(string $passedUri): string
218
    {
219
        //sanitize url
220 66
        $url = filter_var($passedUri, FILTER_SANITIZE_URL);
221
222
        //check for rewrite mode
223 66
        $url = str_replace($this->options['rewriteModeOffRouter'], '', $url);
224
225
        //remove basepath
226 66
        $url = substr($url, strlen($this->options['basePath']));
227
228
        //remove doubled slash
229 66
        $url = str_replace('//', '/', $url);
230
231 66
        return (substr($url, 0, 1) === '/') ? $url : '/'.$url;
232
    }
233
234
    /**
235
     * @var array Allowed Http methods for fast route mapping.
236
     */
237
    private $fastMapMethods = [
238
        'GET' => true,
239
        'POST' => true,
240
        'PUT' => true,
241
        'PATCH' => true,
242
        'DELETE' => true,
243
    ];
244
    
245
    /**
246
     * Map a route.
247
     *
248
     * @param array $route
249
     */
250 25
    public function map(array $route)
251
    {
252 25
        array_push($this->routes, $route);
253 25
    }
254
    
255
    /**
256
     * Fast route mapping.
257
     *
258
     * @param string $name
259
     * @param array $arguments
260
     *
261
     * @return void
262
     *
263
     * @throws BadMethodCallException
264
     */
265 15
    public function __call(string $name, array $arguments)
266
    {
267 15
        $method = strtoupper($name);
268
        
269 15
        if (isset($this->fastMapMethods[$method])) {
270 15
            $this->map($this->createRouteArray($method, $arguments[0], $arguments[1], $arguments[2] ?? []));
271
            
272 15
            return;
273
        }
274
        
275
        throw new BadMethodCallException(__METHOD__.": Router->{$name}() method do not exist.");
276
    }
277
    
278
    /**
279
     * Create route array for previous methods.
280
     *
281
     * @param string   $method
282
     * @param string   $url
283
     * @param callable $callback
284
     * @param array    $options
285
     *
286
     * @return array
287
     */
288 15
    private function createRouteArray(string $method, string $url, callable $callback, array $options) : array
289
    {
290 15
        $routeArray = (new Route([
291 15
            'method'   => $method,
292 15
            'url'      => $url,
293 15
            'callback' => $callback,
294 15
        ]))->toArray();
295
296 15
        $route = array_replace_recursive($routeArray, $options);
297
298 15
        return $route;
299
    }
300
}
301