Passed
Push — master ( 4e3da0...f80a69 )
by Marcio
03:43
created

Router::routerCommand()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
/**
3
 * @Package: Router - simple router class for php
4
 * @Class  : Router
5
 * @Author : izni burak demirtas / @izniburak <[email protected]>
6
 * @Web    : http://burakdemirtas.org
7
 * @URL    : https://github.com/izniburak/php-router
8
 * @Licence: The MIT License (MIT) - Copyright (c) - http://opensource.org/licenses/MIT
9
 */
10
11
namespace Ballybran\Routing;
12
13
use Ballybran\Routing\Router\RouterCommand;
14
use Ballybran\Routing\Router\RouterException;
15
use Ballybran\Routing\Router\RouterRequest;
16
use Ballybran\Routing\Router\RouteMiddleware;
17
use Closure;
18
use Exception;
19
use ReflectionMethod;
20
21
/**
22
 * Class Router
23
 *
24
 * @method $this any($route, $settings, $callback = null)
25
 * @method $this get($route, $settings, $callback = null)
26
 * @method $this post($route, $settings, $callback = null)
27
 * @method $this put($route, $settings, $callback = null)
28
 * @method $this delete($route, $settings, $callback = null)
29
 * @method $this patch($route, $settings, $callback = null)
30
 * @method $this head($route, $settings, $callback = null)
31
 * @method $this options($route, $settings, $callback = null)
32
 * @method $this xpost($route, $settings, $callback = null)
33
 * @method $this xput($route, $settings, $callback = null)
34
 * @method $this xdelete($route, $settings, $callback = null)
35
 * @method $this xpatch($route, $settings, $callback = null)
36
 *
37
 * @package Buki
38
 */
39
class Router extends RouteMiddleware
40
{
41
    /**
42
     * @var string
43
     */
44
    protected $documentRoot = '';
45
46
    /**
47
     * @var string
48
     */
49
    protected $runningPath = '';
50
51
    /**
52
     * @var string $baseFolder Pattern definitions for parameters of Route
53
     */
54
    protected $baseFolder;
55
56
    /**
57
     * @var array $routes Routes list
58
     */
59
    protected $routes = [];
60
61
    /**
62
     * @var array $groups List of group routes
63
     */
64
    protected $groups = [];
65
66
    /**
67
     * @var array $patterns Pattern definitions for parameters of Route
68
     */
69
    protected $patterns = [
70
        ':id' => '(\d+)',
71
        ':number' => '(\d+)',
72
        ':any' => '([^/]+)',
73
        ':all' => '(.*)',
74
        ':string' => '(\w+)',
75
        ':slug' => '([\w\-_]+)',
76
    ];
77
78
    /**
79
     * @var array $namespaces Namespaces of Controllers and Middlewares files
80
     */
81
    protected $namespaces = [
82
        'middlewares' => '',
83
        'controllers' => '',
84
    ];
85
86
    /**
87
     * @var array $path Paths of Controllers and Middlewares files
88
     */
89
    protected $paths = [
90
        'controllers' => 'Controllers',
91
        'middlewares' => 'Middlewares',
92
    ];
93
94
    /**
95
     * @var string $mainMethod Main method for controller
96
     */
97
    protected $mainMethod = 'main';
98
99
    /**
100
     * @var string $cacheFile Cache file
101
     */
102
    protected $cacheFile = null;
103
104
    /**
105
     * @var bool $cacheLoaded Cache is loaded?
106
     */
107
    protected $cacheLoaded = false;
108
109
    /**
110
     * @var Closure $errorCallback Route error callback function
111
     */
112
    protected $errorCallback;
113
114
    /**
115
     * @var array $middlewares General middlewares for per request
116
     */
117
    protected $middlewares = [];
118
119
    /**
120
     * @var array $routeMiddlewares Route middlewares
121
     */
122
    protected $routeMiddlewares = [];
123
124
    /**
125
     * @var array $middlewareGroups Middleware Groups
126
     */
127
    protected $middlewareGroups = [];
128
129
    /**
130
     * Router constructor method.
131
     *
132
     * @param array $params
133
     *
134
     * @return void
135
     */
136
    public function __construct(array $params = [])
137
    {
138
        $this->documentRoot = realpath($_SERVER['DOCUMENT_ROOT']);
139
        $this->runningPath = realpath(getcwd());
140
        $this->baseFolder = $this->runningPath;
141
142
        if (isset($params['debug']) && is_bool($params['debug'])) {
143
            RouterException::$debug = $params['debug'];
144
        }
145
146
        $this->setPaths($params);
147
        $this->loadCache();
148
    }
149
150
    /**
151
     * [TODO] This method implementation not completed yet.
152
     *
153
     * Set route name
154
     *
155
     * @param string $name
156
     *
157
     * @return $this
158
     */
159
    public function name($name)
160
    {
161
        if (!is_string($name)) {
0 ignored issues
show
introduced by
The condition is_string($name) is always true.
Loading history...
162
            return $this;
163
        }
164
165
        $currentRoute = end($this->routes);
166
        $currentRoute['name'] = $name;
167
        array_pop($this->routes);
168
        array_push($this->routes, $currentRoute);
169
170
        return $this;
171
    }
172
173
    /**
174
     * Add route method;
175
     * Get, Post, Put, Delete, Patch, Any, Ajax...
176
     *
177
     * @param $method
178
     * @param $params
179
     *
180
     * @return mixed
181
     * @throws
182
     */
183
    public function __call($method, $params)
184
    {
185
        if ($this->cacheLoaded) {
186
            return true;
187
        }
188
189
        if (is_null($params)) {
190
            return false;
191
        }
192
193
        if (!in_array(strtoupper($method), explode('|', RouterRequest::$validMethods))) {
194
            return $this->exception($method . ' is not valid.');
195
        }
196
197
        $route = $params[0];
198
        $callback = $params[1];
199
        $settings = null;
200
201
        if (count($params) > 2) {
202
            $settings = $params[1];
203
            $callback = $params[2];
204
        }
205
206
        if (strstr($route, ':')) {
207
            $route1 = $route2 = '';
208
            foreach (explode('/', $route) as $key => $value) {
209
                if ($value != '') {
210
                    if (!strpos($value, '?')) {
211
                        $route1 .= '/' . $value;
212
                    } else {
213
                        if ($route2 == '') {
214
                            $this->addRoute($route1, $method, $callback, $settings);
215
                        }
216
217
                        $route2 = $route1 . '/' . str_replace('?', '', $value);
218
                        $this->addRoute($route2, $method, $callback, $settings);
219
                        $route1 = $route2;
220
                    }
221
                }
222
            }
223
224
            if ($route2 == '') {
225
                $this->addRoute($route1, $method, $callback, $settings);
226
            }
227
        } else {
228
            $this->addRoute($route, $method, $callback, $settings);
229
        }
230
231
        return $this;
232
    }
233
234
    /**
235
     * Add new route method one or more http methods.
236
     *
237
     * @param string               $methods
238
     * @param string               $route
239
     * @param array|string|closure $settings
240
     * @param string|closure       $callback
241
     *
242
     * @return bool
243
     */
244
    public function add($methods, $route, $settings, $callback = null)
245
    {
246
        if ($this->cacheLoaded) {
247
            return true;
248
        }
249
250
        if (is_null($callback)) {
251
            $callback = $settings;
252
            $settings = null;
253
        }
254
255
        if (strstr($methods, '|')) {
256
            foreach (array_unique(explode('|', $methods)) as $method) {
257
                if (!empty($method)) {
258
                    call_user_func_array([$this, strtolower($method)], [$route, $settings, $callback]);
259
                }
260
            }
261
        } else {
262
            call_user_func_array([$this, strtolower($methods)], [$route, $settings, $callback]);
263
        }
264
265
        return true;
266
    }
267
268
    /**
269
     * Add new route rules pattern; String or Array
270
     *
271
     * @param string|array $pattern
272
     * @param null|string  $attr
273
     *
274
     * @return mixed
275
     * @throws
276
     */
277
    public function pattern($pattern, $attr = null)
278
    {
279
        if (is_array($pattern)) {
280
            foreach ($pattern as $key => $value) {
281
                if (in_array($key, array_keys($this->patterns))) {
282
                    return $this->exception($key . ' pattern cannot be changed.');
283
                }
284
                $this->patterns[$key] = '(' . $value . ')';
285
            }
286
        } else {
287
            if (in_array($pattern, array_keys($this->patterns))) {
288
                return $this->exception($pattern . ' pattern cannot be changed.');
289
            }
290
            $this->patterns[$pattern] = '(' . $attr . ')';
291
        }
292
293
        return true;
294
    }
295
296
    /**
297
     * Run Routes
298
     *
299
     * @return void
300
     * @throws
301
     */
302
    public function run()
303
    {
304
        $base = str_replace('\\', '/', str_replace($this->documentRoot, '', $this->runningPath));
305
        $uri = rtrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
306
        if ($_SERVER['REQUEST_URI'] !== $_SERVER['PHP_SELF']) {
307
            $uri = str_replace(dirname($_SERVER['PHP_SELF']), '/', $uri);
308
        }
309
310
        if (($base !== $uri) && (substr($uri, -1) === '/')) {
311
            $uri = substr($uri, 0, (strlen($uri) - 1));
312
        }
313
314
        $uri = $this->clearRouteName($uri);
315
        $method = RouterRequest::getRequestMethod();
316
        $searches = array_keys($this->patterns);
317
        $replaces = array_values($this->patterns);
318
        $foundRoute = false;
319
320
        $routes = array_column($this->routes, 'route');
321
322
        // check if route is defined without regex
323
        if (in_array($uri, $routes)) {
324
            $currentRoute = array_filter($this->routes, function($r) use ($method, $uri) {
325
                return RouterRequest::validMethod($r['method'], $method) && $r['route'] === $uri;
326
            });
327
            if (!empty($currentRoute)) {
328
                $currentRoute = current($currentRoute);
329
                $foundRoute = true;
330
                $this->runRouteMiddleware($currentRoute, 'before');
331
                $this->runRouteCommand($currentRoute['callback']);
332
                $this->runRouteMiddleware($currentRoute, 'after');
333
            }
334
        } else {
335
            foreach ($this->routes as $data) {
336
                $route = $data['route'];
337
                if (strstr($route, ':') !== false) {
338
                    $route = str_replace($searches, $replaces, $route);
339
                }
340
341
                if (preg_match('#^' . $route . '$#', $uri, $matched)) {
342
                    if (RouterRequest::validMethod($data['method'], $method)) {
343
                        $foundRoute = true;
344
345
                        $this->runRouteMiddleware($data, 'before');
346
347
                        array_shift($matched);
348
                        $matched = array_map(function($value) {
349
                            return trim(urldecode($value));
350
                        }, $matched);
351
352
                        $this->runRouteCommand($data['callback'], $matched);
353
                        $this->runRouteMiddleware($data, 'after');
354
                        break;
355
                    }
356
                }
357
            }
358
        }
359
360
        // If it originally was a HEAD request, clean up after ourselves by emptying the output buffer
361
        if (strtoupper($_SERVER['REQUEST_METHOD']) === 'HEAD') {
362
            ob_end_clean();
363
        }
364
365
        if ($foundRoute === false) {
0 ignored issues
show
introduced by
The condition $foundRoute === false is always true.
Loading history...
366
            if (!$this->errorCallback) {
367
                $this->errorCallback = function() {
368
                    header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
369
                    return $this->exception('Route not found. Looks like something went wrong. Please try again.');
370
                };
371
            }
372
            call_user_func($this->errorCallback);
373
        }
374
    }
375
376
    /**
377
     * Routes Group
378
     *
379
     * @param string        $name
380
     * @param closure|array $settings
381
     * @param null|closure  $callback
382
     *
383
     * @return bool
384
     */
385
    public function group($name, $settings = null, $callback = null)
386
    {
387
        if ($this->cacheLoaded) {
388
            return true;
389
        }
390
391
        $group = [];
392
        $group['route'] = $this->clearRouteName($name);
393
        $group['before'] = $group['after'] = null;
394
395
        if (is_null($callback)) {
396
            $callback = $settings;
397
        } else {
398
            $group['before'][] = !isset($settings['before']) ? null : $settings['before'];
399
            $group['after'][] = !isset($settings['after']) ? null : $settings['after'];
400
        }
401
402
        $groupCount = count($this->groups);
403
        if ($groupCount > 0) {
404
            $list = [];
405
            foreach ($this->groups as $key => $value) {
406
                if (is_array($value['before'])) {
407
                    foreach ($value['before'] as $k => $v) {
408
                        $list['before'][] = $v;
409
                    }
410
                    foreach ($value['after'] as $k => $v) {
411
                        $list['after'][] = $v;
412
                    }
413
                }
414
            }
415
416
            if (!is_null($group['before'])) {
417
                $list['before'][] = $group['before'][0];
418
            }
419
420
            if (!is_null($group['after'])) {
421
                $list['after'][] = $group['after'][0];
422
            }
423
424
            $group['before'] = $list['before'];
425
            $group['after'] = $list['after'];
426
        }
427
428
        $group['before'] = array_values(array_unique((array)$group['before']));
429
        $group['after'] = array_values(array_unique((array)$group['after']));
430
431
        array_push($this->groups, $group);
432
433
        if (is_object($callback)) {
434
            call_user_func_array($callback, [$this]);
435
        }
436
437
        $this->endGroup();
438
439
        return true;
440
    }
441
442
    /**
443
     * Added route from methods of Controller file.
444
     *
445
     * @param string       $route
446
     * @param string|array $settings
447
     * @param null|string  $controller
448
     *
449
     * @return mixed
450
     * @throws
451
     */
452
    public function controller($route, $settings, $controller = null)
453
    {
454
        if ($this->cacheLoaded) {
455
            return true;
456
        }
457
458
        if (is_null($controller)) {
459
            $controller = $settings;
460
            $settings = [];
461
        }
462
463
        $controller = $this->resolveClass($controller);
464
        $classMethods = get_class_methods($controller);
465
        if ($classMethods) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $classMethods of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
466
            foreach ($classMethods as $methodName) {
467
                if (!strstr($methodName, '__')) {
468
                    $method = 'any';
469
                    foreach (explode('|', RouterRequest::$validMethods) as $m) {
470
                        if (stripos($methodName, strtolower($m), 0) === 0) {
471
                            $method = strtolower($m);
472
                            break;
473
                        }
474
                    }
475
476
                    $methodVar = lcfirst(preg_replace('/' . $method . '/i', '', $methodName, 1));
477
                    $methodVar = strtolower(preg_replace('%([a-z]|[0-9])([A-Z])%', '\1-\2', $methodVar));
478
                    $r = new ReflectionMethod($controller, $methodName);
479
                    $endpoints = [];
480
                    foreach ($r->getParameters() as $param) {
481
                        $pattern = ':any';
482
                        $typeHint = $param->hasType() ? $param->getType()->getName() : null;
483
                        if (in_array($typeHint, ['int', 'bool'])) {
484
                            $pattern = ':id';
485
                        } elseif (in_array($typeHint, ['string', 'float'])) {
486
                            $pattern = ':slug';
487
                        } elseif ($typeHint === null) {
488
                            $pattern = ':any';
489
                        } else {
490
                            continue;
491
                        }
492
                        $endpoints[] = $param->isOptional() ? $pattern . '?' : $pattern;
493
                    }
494
495
                    $value = ($methodVar === $this->mainMethod ? $route : $route . '/' . $methodVar);
496
                    $this->{$method}(
497
                        ($value . '/' . implode('/', $endpoints)),
498
                        $settings,
499
                        ($controller . '@' . $methodName)
0 ignored issues
show
Bug introduced by
Are you sure $controller of type Ballybran\Routing\Router\RouterException|string can be used in concatenation? ( Ignorable by Annotation )

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

499
                        (/** @scrutinizer ignore-type */ $controller . '@' . $methodName)
Loading history...
500
                    );
501
                }
502
            }
503
            unset($r);
504
        }
505
506
        return true;
507
    }
508
509
    /**
510
     * Add new Route and it's settings
511
     *
512
     * @param $uri
513
     * @param $method
514
     * @param $callback
515
     * @param $settings
516
     *
517
     * @return void
518
     */
519
    private function addRoute($uri, $method, $callback, $settings)
520
    {
521
        $groupItem = count($this->groups) - 1;
522
        $group = '';
523
        if ($groupItem > -1) {
524
            foreach ($this->groups as $key => $value) {
525
                $group .= $value['route'];
526
            }
527
        }
528
529
        $path = dirname($_SERVER['PHP_SELF']);
530
        $path = $path === '/' || strpos($this->runningPath, $path) !== 0 ? '' : $path;
531
532
        if (strstr($path, 'index.php')) {
533
            $data = implode('/', explode('/', $path));
534
            $path = str_replace($data, '', $path);
535
        }
536
537
        $route = $path . $group . '/' . trim($uri, '/');
538
        $route = rtrim($route, '/');
539
        if ($route === $path) {
540
            $route .= '/';
541
        }
542
            $this->criateRoute($route, $groupItem, $method, $callback, $settings);
543
        }
544
545
      private function criateRoute($route, $groupItem, $method, $callback, $settings){
546
            $routeName = is_string($callback)
547
            ? strtolower(preg_replace(
548
                '/[^\w]/i', '/', str_replace($this->namespaces['controllers'], '', $callback)
549
            ))
550
            : null;
551
        $data = [
552
            'route' => $this->clearRouteName($route),
553
            'method' => strtoupper($method),
554
            'callback' => $callback,
555
            'name' => isset($settings['name']) ? $settings['name'] : $routeName,
556
            'before' => isset($settings['before']) ? $settings['before'] : null,
557
            'after' => isset($settings['after']) ? $settings['after'] : null,
558
            'group' => $groupItem === -1 ? null : $this->groups[$groupItem],
559
        ];
560
561
        array_push($this->routes, $data);
562
    
563
        }
564
565
    /**
566
     * Run Route Command; Controller or Closure
567
     *
568
     * @param $command
569
     * @param $params
570
     *
571
     * @return void
572
     */
573
    private function runRouteCommand($command, $params = null)
574
    {
575
        $this->routerCommand()->runRoute($command, $params);
576
    }
577
578
    /**
579
     * Routes Group endpoint
580
     *
581
     * @return void
582
     */
583
    private function endGroup()
584
    {
585
        array_pop($this->groups);
586
    }
587
588
     /**
589
     * Detect Routes Middleware; before or after
590
     *
591
     * @param $middleware
592
     * @param $type
593
     *
594
     * @return void
595
     */
596
    public function runRouteMiddleware($middleware, $type)
597
    {
598
        if ($type === 'before') {
599
            if (!is_null($middleware['group'])) {
600
                $this->routerCommand()->beforeAfter($middleware['group'][$type]);
601
            }
602
            $this->routerCommand()->beforeAfter($middleware[$type]);
603
        } else {
604
            $this->routerCommand()->beforeAfter($middleware[$type]);
605
            if (!is_null($middleware['group'])) {
606
                $this->routerCommand()->beforeAfter($middleware['group'][$type]);
607
            }
608
        }
609
    }
610
611
    /**
612
     * @param string $route
613
     *
614
     * @return string
615
     */
616
    private function clearRouteName($route = '')
617
    {
618
        $route = trim(str_replace('//', '/', $route), '/');
619
        return $route === '' ? '/' : "/{$route}";
620
    }
621
}
622