Completed
Branch master (ebca84)
by Alex
04:12 queued 02:34
created

Router.php (2 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)) {
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;
288
        }
289
290
        $paremeters = [];
291
292
        for ($i = 0; $i < count($cleanPattern); $i ++) {
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;
302
                }
303
            } else {
304
                // it's a static part of the route
305
                if ($cleanRoute[$i] !== $cleanPattern[$i]) {
306
                    return false;
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