Router::find()   B
last analyzed

Complexity

Conditions 6
Paths 4

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 18
rs 8.8571
cc 6
eloc 10
nc 4
nop 2
1
<?php
2
3
namespace Routify;
4
5
6
class Router {
7
8
    /**
9
     * @var array The list of routes that contains the callback function and the request method.
10
     */
11
    private $routes;
12
13
    /**
14
     * @var string The path requested for the client(browser, cli, app...).
15
     */
16
    private $path;
17
18
    /**
19
     * @var string The requested method(GET, POST, PUT, DELETE) used by the client.
20
     */
21
    private $requestMethod;
22
23
    /**
24
     * @var object The instance of the router parser.
25
     */
26
    private $routerParser;
27
28
    /**
29
     * @var callable The action to be executed if the any route doesn't match
30
     */
31
    private $notFound;
32
33
    public function __construct() {
0 ignored issues
show
Coding Style introduced by
__construct uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
34
        $this->routes = [];
35
        // only path, without ?, #, etc.
36
        $this->path = (isset($_SERVER['REQUEST_URI'])) ?
37
            rawurldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)) : '/';
38
        $this->routerParser = new RouterParser($this->path);
39
        $this->requestMethod = $_SERVER['REQUEST_METHOD'];
40
        $this->overrideMethod();
41
42
        $this->notFound = function() {};
43
    }
44
45
    /**
46
     * Returns the path.
47
     *
48
     * @return string
49
     */
50
51
    public function getPath() {
52
        return $this->path;
53
    }
54
55
    /**
56
     * Returns the array of routes.
57
     *
58
     * @return array
59
     */
60
61
    public function getRoutes() {
62
        return $this->routes;
63
    }
64
65
    /**
66
     * Return the request method used by the client.
67
     *
68
     * @return string
69
     */
70
71
    public function getRequestMethod() {
72
        return $this->requestMethod;
73
    }
74
75
    /**
76
     * Sets the path. Don't use it in production. Only for tests.
77
     *
78
     * @param $path
79
     */
80
81
    public function setPath($path) {
82
        $this->path = $path;
83
        $this->routerParser->setPath($this->path);
84
    }
85
86
    /**
87
     * Set the request method. Used in tests or development.
88
     *
89
     * @param $method
90
     */
91
92
    public function setRequestMethod($method) {
93
        $this->requestMethod = $method;
94
    }
95
96
    /**
97
     * Dispatch to the route set.
98
     *
99
     * @param $method string Method to dispatch
100
     * @param $path string Path to dispatch
101
     */
102
103
    public function dispatch($method, $path) {
104
        $this->setRequestMethod($method);
105
        $this->setPath($path);
106
    }
107
108
    /**
109
     * Searchs in the array of routes the route that matches(same URI
110
     * and request method).
111
     *
112
     * @param $uri string
113
     * @param $method string The request method
114
     * @return bool true if exist a result
115
     */
116
117
    public function find($uri, $method) {
118
        $found = false;
119
        $counter = 0;
120
121
        if(count($this->routes) === 0) {
122
            return $found;
123
        }
124
125
        while($found === false && $counter < count($this->routes)) {
126
            if($this->routes[$counter]->getUri() === $uri && $this->routes[$counter]->getMethod() === $method) {
127
                $found = true;
128
            }
129
130
            $counter++;
131
        }
132
133
        return $found;
134
    }
135
136
    /**
137
     * Adds a new route to the routes array.
138
     *
139
     * @param $uri
140
     * @param $method
141
     * @param $response
142
     * @return bool false if the route has not been added.
143
     */
144
145
    private function addRoute($uri, $method, $response, array $middleware = []) {
146
        if($this->find($uri, $method)) {    // search if exists an apparition
147
            return false;
148
        }
149
150
        $totalRoutes = count($this->routes);
151
        $this->routes[$totalRoutes] = new Order($uri, $method, $response, $middleware);
152
        return true;
153
    }
154
155
    /**
156
     * Get all request headers.
157
     *
158
     * @return array The request headers
159
     */
160
    private function getRequestHeaders() {
161
        // If getallheaders() is available, use that
162
        if(function_exists('getallheaders')) {
163
            return getallheaders();
164
        }
165
        // Method getallheaders() not available: manually extract 'm
166
        $headers = [];
167
        foreach(filter_input_array(INPUT_SERVER) as $key => $value) {
168
            if((substr($key, 0, 5) == 'HTTP_') || ($key == 'CONTENT_TYPE') || ($key == 'CONTENT_LENGTH')) {
169
                $headers[str_replace([' ', 'Http'], ['-', 'HTTP'], ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))))] = $value;
170
            }
171
        }
172
173
        return $headers;
174
    }
175
176
    /**
177
     * Get the request method used, taking overrides into account.
178
     *
179
     */
180
181
    private function overrideMethod() {
182
        if($this->requestMethod === 'HEAD') {
183
            $this->requestMethod = 'GET';
184
        }elseif($this->requestMethod === 'POST' && filter_input(INPUT_POST, '_method') !== null) {
185
            $this->requestMethod = filter_input(INPUT_POST, '_method');
186
        }elseif($this->requestMethod === 'POST') {
187
            $headers = $this->getRequestHeaders();
188
            if(isset($headers['X-HTTP-Method-Override']) && in_array($headers['X-HTTP-Method-Override'], ['PUT', 'DELETE', 'PATCH'])) {
189
                $this->requestMethod = $headers['X-HTTP-Method-Override'];
190
            }
191
        }
192
    }
193
194
    /**
195
     * Clear the array of orders(routes) registered.
196
     */
197
198
    public function clear() {
199
        $this->routes = [];
200
    }
201
202
    /**
203
     * Register the GET request.
204
     *
205
     * @param $uri
206
     * @param $response
207
     */
208
209
    public function get($uri, $response, array $middleware = []) {
210
        $this->addRoute($uri, Method::GET, $response, $middleware);
211
    }
212
213
    /**
214
     * Register the POST request.
215
     *
216
     * @param $uri
217
     * @param $response
218
     */
219
220
    public function post($uri, $response, array $middleware = []) {
221
        $this->addRoute($uri, Method::POST, $response, $middleware);
222
    }
223
224
    /**
225
     * Register the PUT request.
226
     *
227
     * @param $uri
228
     * @param $response
229
     */
230
231
    public function put($uri, $response, array $middleware = []) {
232
        $this->addRoute($uri, Method::PUT, $response, $middleware);
233
    }
234
235
    /**
236
     * Register the DELETE request.
237
     *
238
     * @param $uri string The path requested
239
     * @param $response callable Action to response
240
     */
241
242
    public function delete($uri, $response, array $middleware = []) {
243
        $this->addRoute($uri, Method::DELETE, $response, $middleware);
244
    }
245
246
    /**
247
     * Register the PATCH request.
248
     *
249
     * @param $uri string The path requested
250
     * @param $response callable Action to response
251
     */
252
253
    public function patch($uri, $response, array $middleware = []) {
254
        $this->addRoute($uri, Method::PATCH, $response, $middleware);
255
    }
256
257
    /**
258
     * Register one or more requests for the same uri.
259
     *
260
     * @param $uri string The path requested
261
     * @param $response callable Action to response
262
     * @param $methods mixed Methods to bind. Optional. GET by default
263
     * @param $middleware array Middlewares before and after. Optional
264
     */
265
266
    public function both($uri, $response, $methods = Method::GET, array $middleware = []) {
267
        if(is_array($methods)) {
268
            foreach($methods as $method) {
269
                $this->addRoute($uri, $method, $response, $middleware);
270
            }
271
        } else {
272
            $this->addRoute($uri, $methods, $response, $middleware);
273
        }
274
    }
275
276
    /**
277
     *
278
     */
279
280
    public function any($uri, $response, array $middleware = []) {
281
        $this->addRoute($uri, Method::GET, $response, $middleware);
282
        $this->addRoute($uri, Method::POST, $response, $middleware);
283
        $this->addRoute($uri, Method::PUT, $response, $middleware);
284
        $this->addRoute($uri, Method::DELETE, $response, $middleware);
285
        $this->addRoute($uri, Method::PATCH, $response, $middleware);
286
    }
287
288
    /**
289
     * Sets a callback for the notFound event.
290
     *
291
     * @param $func callable
292
     */
293
294
    public function notFound($func) {
295
        if(is_callable($func)) {
296
            $this->notFound = $func;
297
        }
298
    }
299
300
    /**
301
     * Initialize the router app and start to run
302
     * over the array of routes for any appearance.
303
     * If there is a result then call the callback associate.
304
     * If there is not a result it will execute the notFound
305
     * action.
306
     *
307
     * @return mixed The result of execute the callback.
308
     */
309
310
    public function run() {
311
        $found = false;
312
        $counter = 0;
313
        while($found === false && $counter < count($this->routes)) {
314
            if($this->routerParser->match($this->routes[$counter]->getUri()) && $this->routes[$counter]->getMethod() === $this->requestMethod) {
315
                $found = true;
316
            } else {
317
                $counter++;
318
            }
319
        }
320
321
        if($found) {
322
            // run the before middleware if it exists
323
            if($this->routes[$counter]->hasBefore()) {
324
                call_user_func($this->routes[$counter]->getMiddlewares()['before']);
325
            }
326
327
            $params = $this->routerParser->getParams($this->routes[$counter]->getUri());
328
            $response = call_user_func_array($this->routes[$counter]->getResponse(), $params);
329
330
            // run the after middleware if it exists
331
            if($this->routes[$counter]->hasAfter()) {
332
                call_user_func($this->routes[$counter]->getMiddlewares()['after']);
333
            }
334
335
            return $response;
336
        } else {
337
            return call_user_func($this->notFound);
338
        }
339
    }
340
}