Completed
Push — b0.25.0 ( 535bde...57b0c8 )
by Sebastian
07:17
created

Router::createRouteArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 11
ccs 8
cts 8
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 4
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 boot Use of url rewriting.
0 ignored issues
show
Bug introduced by
The type Linna\Router\boot was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
     * Constructor.
70
     * Accept as parameter a list routes and options.
71
     *
72
     * @param array $routes
73
     * @param array $options
74
     */
75 69
    public function __construct(array $routes, array $options = [])
76
    {
77
        [
78 69
            'basePath'             => $this->basePath,
79 69
            'badRoute'             => $this->badRoute,
80 69
            'rewriteMode'          => $this->rewriteMode,
81 69
            'rewriteModeOffRouter' => $this->rewriteModeOffRouter
82 69
        ] = array_replace_recursive([
83 69
            'basePath'             => '/',
84
            'badRoute'             => false,
85
            'rewriteMode'          => false,
86
            'rewriteModeOffRouter' => '/index.php?'
87 69
        ], $options);
88
89
        //set routes
90 69
        $this->routes = $routes;
91 69
    }
92
93
    /**
94
     * Evaluate request uri.
95
     *
96
     * @param string $requestUri
97
     * @param string $requestMethod
98
     *
99
     * @return bool
100
     */
101 49
    public function validate(string $requestUri, string $requestMethod): bool
102
    {
103 49
        $route = $this->findRoute($this->filterUri($requestUri), $requestMethod);
104
105 49
        if ($route instanceof NullRoute) {
106 6
            $this->buildErrorRoute();
107 6
            return false;
108
        }
109
110 43
        $this->buildValidRoute($route);
0 ignored issues
show
Bug introduced by
$route of type Linna\Router\RouteInterface is incompatible with the type array expected by parameter $route of Linna\Router\Router::buildValidRoute(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

110
        $this->buildValidRoute(/** @scrutinizer ignore-type */ $route);
Loading history...
111
112 43
        return true;
113
    }
114
115
    /**
116
     * Find if provided route match with one of registered routes.
117
     *
118
     * @param string $uri
119
     * @param string $method
120
     *
121
     * @return RouteInterface
122
     */
123 49
    private function findRoute(string $uri, string $method): RouteInterface
124
    {
125 49
        $matches = [];
126 49
        $route = new NullRoute();
127
128 49
        foreach ($this->routes as $value) {
129 49
            $urlMatch = preg_match('`^'.preg_replace($this->matchTypes, $this->types, $value->url).'/?$`', $uri, $matches);
130 49
            $methodMatch = strpos($value->method, $method);
131
132 49
            if ($urlMatch && $methodMatch !== false) {
133 43
                $route = $value;
134 43
                $route->matches = $matches;
135 49
                break;
136
            }
137
        }
138
139 49
        return $route;
140
    }
141
142
    /**
143
     * Build a valid route.
144
     *
145
     * @param array $route
146
     */
147 43
    private function buildValidRoute($route): void
148
    {
149
        //add to route array the passed uri for param check when call
150 43
        $matches = $route->matches;
151
152
        //route match and there is a subpattern with action
153 43
        if (count($matches) > 1) {
154
            //assume that subpattern rapresent action
155 10
            $route->action = $matches[1];
156
157
            //url clean
158 10
            $route->url = preg_replace('`\([0-9A-Za-z\|]++\)`', $matches[1], $route->url);
159
        }
160
161 43
        $route->param = $this->buildParam($route);
162
163
        //delete matches key because not required inside route object
164 43
        unset($route->matches);
165
166 43
        $this->route = $route;
0 ignored issues
show
Documentation Bug introduced by
It seems like $route of type array is incompatible with the declared type Linna\Router\RouteInterface of property $route.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
167 43
    }
168
169
    /**
170
     * Try to find parameters in a valid route and return it.
171
     *
172
     * @param array $route
173
     *
174
     * @return array
175
     */
176 43
    private function buildParam($route): array
177
    {
178 43
        $param = [];
179
180 43
        $url = explode('/', $route->url);
181 43
        $matches = explode('/', $route->matches[0]);
182
183 43
        $rawParam = array_diff($matches, $url);
184
185 43
        foreach ($rawParam as $key => $value) {
186 22
            $paramName = strtr($url[$key], ['[' => '', ']' => '']);
187 22
            $param[$paramName] = $value;
188
        }
189
190 43
        return $param;
191
    }
192
193
    /**
194
     * Actions for error route.
195
     *
196
     * @return void
197
     */
198 6
    private function buildErrorRoute(): void
199
    {
200
        //check if there is a declared route for errors, if no exit with false
201 6
        if (($key = array_search($this->badRoute, array_column($this->routes, 'name'), true)) === false) {
202 2
            $this->route = new NullRoute();
203
204 2
            return;
205
        }
206
207
        //build and store route for errors
208 4
        $this->route = $this->routes[$key];
209 4
    }
210
211
    /**
212
     * Check if a route is valid and
213
     * return the route object else return a bad route object.
214
     *
215
     * @return RouteInterface
216
     */
217 49
    public function getRoute(): RouteInterface
218
    {
219 49
        return $this->route;
220
    }
221
222
    /**
223
     * Analize $_SERVER['REQUEST_URI'] for current uri, sanitize and return it.
224
     *
225
     * @param string $passedUri
226
     *
227
     * @return string
228
     */
229 49
    private function filterUri(string $passedUri): string
230
    {
231
        //sanitize url
232 49
        $url = filter_var($passedUri, FILTER_SANITIZE_URL);
233
234
        //check for rewrite mode
235 49
        $url = str_replace($this->rewriteModeOffRouter, '', $url);
236
237
        //remove basepath
238 49
        $url = substr($url, strlen($this->basePath));
239
240
        //remove doubled slash
241 49
        $url = str_replace('//', '/', $url);
242
243 49
        return (substr($url, 0, 1) === '/') ? $url : '/'.$url;
244
    }
245
246
    /**
247
     * Map a route.
248
     *
249
     * @param RouteInterface $route
250
     */
251 15
    public function map(RouteInterface $route): void
252
    {
253 15
        array_push($this->routes, $route);
254 15
    }
255
256
    /**
257
     * Fast route mapping.
258
     *
259
     * @param string $name
260
     * @param array  $arguments
261
     *
262
     * @return void
263
     *
264
     * @throws BadMethodCallException
265
     */
266 11
    public function __call(string $name, array $arguments)
267
    {
268 11
        $method = strtoupper($name);
269
270 11
        if (in_array($method, ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])) {
271 10
            $this->map(
272 10
                new Route(array_merge($arguments[2] ?? [], [
273 10
                    'method'   => $method,
274 10
                    'url'      => $arguments[0],
275 10
                    'callback' => $arguments[1]
276
                ]))
277
            );
278
279 10
            return;
280
        }
281
282 1
        throw new BadMethodCallException("Router->{$name}() method do not exist.");
283
    }
284
}
285