Completed
Push — b0.25.0 ( 08d9c9...6b8d05 )
by Sebastian
04:56
created

Router::buildParam()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 15
ccs 9
cts 9
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
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
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 74
    public function __construct(array $routes = [], array $options = [])
69
    {
70
        //set options
71 74
        $this->setOptions($options);
72
73
        //set routes
74 74
        $this->routes = $routes;
75 74
    }
76
77
    /**
78
     * Evaluate request uri.
79
     *
80
     * @param string $requestUri
81
     * @param string $requestMethod
82
     *
83
     * @return bool
84
     */
85 49
    public function validate(string $requestUri, string $requestMethod): bool
86
    {
87 49
        $route = $this->findRoute($this->filterUri($requestUri), $requestMethod);
88
89 49
        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 43
            $this->buildValidRoute($route);
91
92 43
            return true;
93
        }
94
95 6
        $this->buildErrorRoute();
96
97 6
        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 49
    private function findRoute(string $uri, string $method): array
109
    {
110 49
        $matches = [];
111 49
        $route = [];
112
113 49
        foreach ($this->routes as $value) {
114 49
            $urlMatch = preg_match('`^'.preg_replace($this->matchTypes, $this->types, $value['url']).'/?$`', $uri, $matches);
115 49
            $methodMatch = strpos($value['method'], $method);
116
117 49
            if ($urlMatch && $methodMatch !== false) {
118 43
                $route = $value;
119 43
                $route['matches'] = $matches;
120 49
                break;
121
            }
122
        }
123
124 49
        return $route;
125
    }
126
127
    /**
128
     * Build a valid route.
129
     *
130
     * @param array $route
131
     */
132 43
    private function buildValidRoute(array $route): void
133
    {
134
        //add to route array the passed uri for param check when call
135 43
        $matches = $route['matches'];
136
137
        //route match and there is a subpattern with action
138 43
        if (count($matches) > 1) {
139
            //assume that subpattern rapresent action
140 10
            $route['action'] = $matches[1];
141
142
            //url clean
143 10
            $route['url'] = preg_replace('`\([0-9A-Za-z\|]++\)`', $matches[1], $route['url']);
144
        }
145
146 43
        $route['param'] = $this->buildParam($route);
147
148
        //delete matches key because not required inside route object
149 43
        unset($route['matches']);
150
151 43
        $this->route = new Route($route);
152 43
    }
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 43
    private function buildParam(array $route): array
162
    {
163 43
        $param = [];
164
165 43
        $url = explode('/', $route['url']);
166 43
        $matches = explode('/', $route['matches'][0]);
167
168 43
        $rawParam = array_diff($matches, $url);
169
170 43
        foreach ($rawParam as $key => $value) {
171 22
            $paramName = strtr($url[$key], ['[' => '', ']' => '']);
172 22
            $param[$paramName] = $value;
173
        }
174
175 43
        return $param;
176
    }
177
178
    /**
179
     * Actions for error route.
180
     *
181
     * @return void
182
     */
183 6
    private function buildErrorRoute(): void
184
    {
185
        //check if there is a declared route for errors, if no exit with false
186 6
        if (($key = array_search($this->options['badRoute'], array_column($this->routes, 'name'), true)) === false) {
187 2
            $this->route = new NullRoute();
188
189 2
            return;
190
        }
191
192
        //pick route for errors
193 4
        $route = $this->routes[$key];
194
195
        //build and store route for errors
196 4
        $this->route = new Route($route);
197 4
    }
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 49
    public function getRoute(): RouteInterface
206
    {
207 49
        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 49
    private function filterUri(string $passedUri): string
218
    {
219
        //sanitize url
220 49
        $url = filter_var($passedUri, FILTER_SANITIZE_URL);
221
222
        //check for rewrite mode
223 49
        $url = str_replace($this->options['rewriteModeOffRouter'], '', $url);
224
225
        //remove basepath
226 49
        $url = substr($url, strlen($this->options['basePath']));
227
228
        //remove doubled slash
229 49
        $url = str_replace('//', '/', $url);
230
231 49
        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 15
    public function map(array $route): void
251
    {
252 15
        array_push($this->routes, $route);
253 15
    }
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 11
    public function __call(string $name, array $arguments)
266
    {
267 11
        $method = strtoupper($name);
268
269 11
        if (isset($this->fastMapMethods[$method])) {
270 10
            $this->map($this->createRouteArray($method, $arguments[0], $arguments[1], $arguments[2] ?? []));
271
272 10
            return;
273
        }
274
275 1
        throw new BadMethodCallException("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 10
    private function createRouteArray(string $method, string $url, callable $callback, array $options): array
289
    {
290 10
        $routeArray = (new Route([
291 10
            'method'   => $method,
292 10
            'url'      => $url,
293 10
            'callback' => $callback,
294 10
        ]))->toArray();
295
296 10
        $route = array_replace_recursive($routeArray, $options);
297
298 10
        return $route;
299
    }
300
}
301