Completed
Push — master ( 776184...43ac10 )
by Alex
01:28
created

Router.php (1 issue)

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