Completed
Branch master (7ba38a)
by Alex
02:54 queued 01:08
created

Router.php (7 issues)

1
<?php
2
namespace Mezon\Router;
3
4
// TODO compare speed with klein
5
// TODO compare speed with Symphony router
6
// TODO [create|edit:action]
7
// TODO /date/[i:year]-[i:month]-[i:day]
8
9
/**
10
 * Class Router
11
 *
12
 * @package Mezon
13
 * @subpackage Router
14
 * @author Dodonov A.A.
15
 * @version v.1.0 (2019/08/15)
16
 * @copyright Copyright (c) 2019, aeon.org
17
 */
18
19
/**
20
 * Router class
21
 */
22
class Router
23
{
24
25
    /**
26
     * Mapping of routes to their execution functions for GET requests
27
     *
28
     * @var array
29
     */
30
    private $getRoutes = [];
31
32
    /**
33
     * Mapping of routes to their execution functions for GET requests
34
     *
35
     * @var array
36
     */
37
    private $postRoutes = [];
38
39
    /**
40
     * Mapping of routes to their execution functions for PUT requests
41
     *
42
     * @var array
43
     */
44
    private $putRoutes = [];
45
46
    /**
47
     * Mapping of routes to their execution functions for DELETE requests
48
     *
49
     * @var array
50
     */
51
    private $deleteRoutes = [];
52
53
    /**
54
     * Method wich handles invalid route error
55
     *
56
     * @var callable
57
     */
58
    private $invalidRouteErrorHandler;
59
60
    /**
61
     * Parsed parameters of the calling router
62
     *
63
     * @var array
64
     */
65
    protected $parameters = [];
66
67
    /**
68
     * Method returns request method
69
     *
70
     * @return string Request method
71
     */
72
    protected function getRequestMethod(): string
73
    {
74
        return $_SERVER['REQUEST_METHOD'] ?? 'GET';
75
    }
76
77
    /**
78
     * Constructor
79
     */
80
    public function __construct()
81
    {
82
        $_SERVER['REQUEST_METHOD'] = $this->getRequestMethod();
83
84
        $this->invalidRouteErrorHandler = [
85
            $this,
86
            'noProcessorFoundErrorHandler'
87
        ];
88
    }
89
90
    /**
91
     * Method fetches actions from the objects and creates GetRoutes for them
92
     *
93
     * @param object $object
94
     *            Object to be processed
95
     */
96
    public function fetchActions(object $object): void
97
    {
98
        $methods = get_class_methods($object);
99
100
        foreach ($methods as $method) {
101
            if (strpos($method, 'action') === 0) {
102
                $route = \Mezon\Router\Utils::convertMethodNameToRoute($method);
103
                $this->getRoutes["/$route/"] = [
104
                    $object,
105
                    $method
106
                ];
107
                $this->postRoutes["/$route/"] = [
108
                    $object,
109
                    $method
110
                ];
111
            }
112
        }
113
    }
114
115
    /**
116
     * Method adds route and it's handler
117
     *
118
     * $callback function may have two parameters - $route and $parameters. Where $route is a called route,
119
     * and $parameters is associative array (parameter name => parameter value) with URL parameters
120
     *
121
     * @param string $route
122
     *            Route
123
     * @param mixed $callback
124
     *            Collback wich will be processing route call.
125
     * @param string $requestMethod
126
     *            Request type
127
     */
128
    public function addRoute(string $route, $callback, $requestMethod = 'GET'): void
129
    {
130
        $route = '/' . trim($route, '/') . '/';
131
132
        if (is_array($requestMethod)) {
0 ignored issues
show
The condition is_array($requestMethod) is always false.
Loading history...
133
            foreach ($requestMethod as $r) {
134
                $this->addRoute($route, $callback, $r);
135
            }
136
        } else {
137
            $routes = &$this->_getRoutesForMethod($requestMethod);
138
            // this 'if' is for backward compatibility
139
            // remove it on 02-04-2021
140
            if (is_array($callback) && isset($callback[1]) && is_array($callback[1])) {
141
                $callback = $callback[1];
142
            }
143
            $routes[$route] = $callback;
144
        }
145
    }
146
147
    /**
148
     * Method searches route processor
149
     *
150
     * @param mixed $processors
151
     *            Callable router's processor
152
     * @param string $route
153
     *            Route
154
     * @return mixed Result of the router processor
155
     */
156
    private function _findStaticRouteProcessor(&$processors, string $route)
157
    {
158
        foreach ($processors as $i => $processor) {
159
            // exact router or 'all router'
160
            if ($i == $route || $i == '/*/') {
161
                if (is_callable($processor) && is_array($processor) === false) {
162
                    return $processor($route, []);
163
                }
164
165
                $functionName = $processor[1] ?? null;
166
167
                if (is_callable($processor) &&
168
                    (method_exists($processor[0], $functionName) || isset($processor[0]->$functionName))) {
169
                    // passing route path and parameters
170
                    return call_user_func($processor, $route, []);
171
                } else {
172
                    $callableDescription = \Mezon\Router\Utils::getCallableDescription($processor);
173
174
                    if (isset($processor[0]) && method_exists($processor[0], $functionName) === false) {
175
                        throw (new \Exception("'$callableDescription' does not exists"));
176
                    } else {
177
                        throw (new \Exception("'$callableDescription' must be callable entity"));
178
                    }
179
                }
180
            }
181
        }
182
183
        return false;
184
    }
185
186
    /**
187
     * Method returns list of routes for the HTTP method.
188
     *
189
     * @param string $method
190
     *            HTTP Method
191
     * @return array Routes
192
     */
193
    private function &_getRoutesForMethod(string $method): array
194
    {
195
        switch ($method) {
196
            case ('GET'):
197
                $result = &$this->getRoutes;
198
                break;
199
200
            case ('POST'):
201
                $result = &$this->postRoutes;
202
                break;
203
204
            case ('PUT'):
205
                $result = &$this->putRoutes;
206
                break;
207
208
            case ('DELETE'):
209
                $result = &$this->deleteRoutes;
210
                break;
211
212
            default:
213
                throw (new \Exception('Unsupported request method'));
214
        }
215
216
        return $result;
217
    }
218
219
    /**
220
     * Method tries to process static routes without any parameters
221
     *
222
     * @param string $route
223
     *            Route
224
     * @return mixed Result of the router processor
225
     */
226
    private function _tryStaticRoutes($route)
227
    {
228
        $routes = $this->_getRoutesForMethod($this->getRequestMethod());
229
230
        return $this->_findStaticRouteProcessor($routes, $route);
231
    }
232
233
    /**
234
     * Matching parameter and component
235
     *
236
     * @param mixed $component
237
     *            Component of the URL
238
     * @param string $parameter
239
     *            Parameter to be matched
240
     * @return string Matched url parameter
241
     */
242
    private function _matchParameterAndComponent(&$component, string $parameter)
243
    {
244
        $parameterData = explode(':', trim($parameter, '[]'));
245
        $return = '';
246
247
        switch ($parameterData[0]) {
248
            case ('i'):
249
                if (is_numeric($component)) {
250
                    $component = $component + 0;
251
                    $return = $parameterData[1];
252
                }
253
                break;
254
            case ('a'):
255
                if (preg_match('/^([a-z0-9A-Z_\/\-\.\@]+)$/', $component)) {
256
                    $return = $parameterData[1];
257
                }
258
                break;
259
            case ('il'):
260
                if (preg_match('/^([0-9,]+)$/', $component)) {
261
                    $return = $parameterData[1];
262
                }
263
                break;
264
            case ('s'):
265
                $component = htmlspecialchars($component, ENT_QUOTES);
266
                $return = $parameterData[1];
267
                break;
268
            default:
269
                throw (new \Exception('Illegal parameter type/value : ' . $parameterData[0]));
270
        }
271
272
        return $return;
273
    }
274
275
    /**
276
     * Method matches route and pattern
277
     *
278
     * @param array $cleanRoute
279
     *            Cleaned route splitted in parts
280
     * @param array $cleanPattern
281
     *            Route pattern
282
     * @return array Array of route's parameters
283
     */
284
    private function _matchRouteAndPattern(array $cleanRoute, array $cleanPattern)
285
    {
286
        if (count($cleanRoute) !== count($cleanPattern)) {
287
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
288
        }
289
290
        $paremeters = [];
291
292
        for ($i = 0; $i < count($cleanPattern); $i ++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
293
            if (\Mezon\Router\Utils::isParameter($cleanPattern[$i])) {
294
                $parameterName = $this->_matchParameterAndComponent($cleanRoute[$i], $cleanPattern[$i]);
295
296
                // it's a parameter
297
                if ($parameterName !== '') {
298
                    // parameter was matched, store it!
299
                    $paremeters[$parameterName] = $cleanRoute[$i];
300
                } else {
301
                    return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
302
                }
303
            } else {
304
                // it's a static part of the route
305
                if ($cleanRoute[$i] !== $cleanPattern[$i]) {
306
                    return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
307
                }
308
            }
309
        }
310
311
        $this->parameters = $paremeters;
312
    }
313
314
    /**
315
     * Method searches dynamic route processor
316
     *
317
     * @param array $processors
318
     *            Callable router's processor
319
     * @param string $route
320
     *            Route
321
     * @return string|bool Result of the router'scall or false if any error occured
322
     */
323
    private function _findDynamicRouteProcessor(array &$processors, string $route)
324
    {
325
        $cleanRoute = explode('/', trim($route, '/'));
326
327
        foreach ($processors as $i => $processor) {
328
            $cleanPattern = explode('/', trim($i, '/'));
329
330
            if ($this->_matchRouteAndPattern($cleanRoute, $cleanPattern) !== false) {
331
                return call_user_func($processor, $route, $this->parameters); // return result of the router
332
            }
333
        }
334
335
        return false;
336
    }
337
338
    /**
339
     * Method tries to process dynamic routes with parameters
340
     *
341
     * @param string $route
342
     *            Route
343
     * @return string Result of the route call
344
     */
345
    private function _tryDynamicRoutes(string $route)
346
    {
347
        switch ($this->getRequestMethod()) {
348
            case ('GET'):
349
                $result = $this->_findDynamicRouteProcessor($this->getRoutes, $route);
350
                break;
351
352
            case ('POST'):
353
                $result = $this->_findDynamicRouteProcessor($this->postRoutes, $route);
354
                break;
355
356
            case ('PUT'):
357
                $result = $this->_findDynamicRouteProcessor($this->putRoutes, $route);
358
                break;
359
360
            case ('DELETE'):
361
                $result = $this->_findDynamicRouteProcessor($this->deleteRoutes, $route);
362
                break;
363
364
            default:
365
                throw (new \Exception('Unsupported request method'));
366
        }
367
368
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type boolean which is incompatible with the documented return type string.
Loading history...
369
    }
370
371
    /**
372
     * Method rturns all available routes
373
     */
374
    private function _getAllRoutesTrace()
375
    {
376
        return (count($this->getRoutes) ? 'GET:' . implode(', ', array_keys($this->getRoutes)) . '; ' : '') .
377
            (count($this->postRoutes) ? 'POST:' . implode(', ', array_keys($this->postRoutes)) . '; ' : '') .
378
            (count($this->putRoutes) ? 'PUT:' . implode(', ', array_keys($this->putRoutes)) . '; ' : '') .
379
            (count($this->deleteRoutes) ? 'DELETE:' . implode(', ', array_keys($this->deleteRoutes)) : '');
380
    }
381
382
    /**
383
     * Method processes no processor found error
384
     *
385
     * @param string $route
386
     *            Route
387
     */
388
    public function noProcessorFoundErrorHandler(string $route)
389
    {
390
        throw (new \Exception(
391
            'The processor was not found for the route ' . $route . ' in ' . $this->_getAllRoutesTrace()));
392
    }
393
394
    /**
395
     * Method sets InvalidRouteErrorHandler function
396
     *
397
     * @param callable $function
398
     *            Error handler
399
     */
400
    public function setNoProcessorFoundErrorHandler(callable $function)
401
    {
402
        $oldErrorHandler = $this->invalidRouteErrorHandler;
403
404
        $this->invalidRouteErrorHandler = $function;
405
406
        return $oldErrorHandler;
407
    }
408
409
    /**
410
     * Processing specified router
411
     *
412
     * @param string $route
413
     *            Route
414
     */
415
    public function callRoute($route)
416
    {
417
        $route = \Mezon\Router\Utils::prepareRoute($route);
418
419
        if (($result = $this->_tryStaticRoutes($route)) !== false) {
420
            return $result;
421
        }
422
423
        if (($result = $this->_tryDynamicRoutes($route)) !== false) {
0 ignored issues
show
The condition $result = $this->_tryDyn...outes($route) !== false is always true.
Loading history...
424
            return $result;
425
        }
426
427
        call_user_func($this->invalidRouteErrorHandler, $route);
428
    }
429
430
    /**
431
     * Method clears router data.
432
     */
433
    public function clear()
434
    {
435
        $this->getRoutes = [];
436
437
        $this->postRoutes = [];
438
439
        $this->putRoutes = [];
440
441
        $this->deleteRoutes = [];
442
    }
443
444
    /**
445
     * Method returns route parameter
446
     *
447
     * @param string $name
448
     *            Route parameter
449
     * @return string Route parameter
450
     */
451
    public function getParam(string $name): string
452
    {
453
        if (isset($this->parameters[$name]) === false) {
454
            throw (new \Exception('Paremeter ' . $name . ' was not found in route', - 1));
455
        }
456
457
        return $this->parameters[$name];
458
    }
459
460
    /**
461
     * Does parameter exists
462
     *
463
     * @param string $name
464
     *            Param name
465
     * @return bool True if the parameter exists
466
     */
467
    public function hasParam(string $name): bool
468
    {
469
        return isset($this->parameters[$name]);
470
    }
471
472
    /**
473
     * Method returns true if the router exists
474
     *
475
     * @param string $route
476
     *            checking route
477
     * @return bool true if the router exists, false otherwise
478
     */
479
    public function routeExists(string $route): bool
480
    {
481
        $allRoutes = array_merge($this->deleteRoutes, $this->putRoutes, $this->postRoutes, $this->getRoutes);
482
483
        return isset($allRoutes[$route]);
484
    }
485
}
486