Router::compileRoute()   A
last analyzed

Complexity

Conditions 5
Paths 1

Size

Total Lines 44
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 5
eloc 24
c 1
b 0
f 1
nc 1
nop 1
dl 0
loc 44
rs 9.2248
1
<?php
2
/**
3
 * Klein (klein.php) - A fast & flexible router for PHP
4
 *
5
 * @author      Chris O'Hara <[email protected]>
6
 * @author      Trevor Suarez (Rican7) (contributor and v2 refactorer)
7
 * @copyright   (c) Chris O'Hara
8
 * @link        https://github.com/klein/klein.php
9
 * @license     MIT
10
 */
11
12
namespace app\framework\Component\Routing;
13
14
use app\framework\Component\Http\MiddlewareInterface;
15
use Exception;
16
use app\framework\Component\StdLib\SingletonTrait;
17
use app\framework\Component\Routing\DataCollection\RouteCollection;
18
use app\framework\Component\Routing\Exceptions\DispatchHaltedException;
19
use app\framework\Component\Routing\Exceptions\HttpException;
20
use app\framework\Component\Routing\Exceptions\HttpExceptionInterface;
21
use app\framework\Component\Routing\Exceptions\LockedResponseException;
22
use app\framework\Component\Routing\Exceptions\RegularExpressionCompilationException;
23
use app\framework\Component\Routing\Exceptions\RoutePathCompilationException;
24
use app\framework\Component\Routing\Exceptions\UnhandledException;
25
use OutOfBoundsException;
26
use SplQueue;
27
use SplStack;
28
use Throwable;
29
30
/**
31
 * Router
32
 *
33
 * Main Klein router class
34
 * @internal Renamed by MrFibunacci
35
 * @package app\framework\Component\Routing
36
 */
37
class Router
38
{
39
    use SingletonTrait;
40
41
    /**
42
     * The regular expression used to compile and match URL's
43
     *
44
     * @type string
45
     */
46
    const ROUTE_COMPILE_REGEX = '`(\\\?(?:/|\.|))(?:\[([^:\]]*)(?::([^:\]]*))?\])(\?|)`';
47
48
    /**
49
     * The regular expression used to escape the non-named param section of a route URL
50
     *
51
     * @type string
52
     */
53
    const ROUTE_ESCAPE_REGEX = '`(?<=^|\])[^\]\[\?]+?(?=\[|$)`';
54
55
    /**
56
     * Dispatch route output handling
57
     *
58
     * Don't capture anything. Behave as normal.
59
     *
60
     * @type int
61
     */
62
    const DISPATCH_NO_CAPTURE = 0;
63
64
    /**
65
     * Dispatch route output handling
66
     *
67
     * Capture all output and return it from dispatch
68
     *
69
     * @type int
70
     */
71
    const DISPATCH_CAPTURE_AND_RETURN = 1;
72
73
    /**
74
     * Dispatch route output handling
75
     *
76
     * Capture all output and replace the response body with it
77
     *
78
     * @type int
79
     */
80
    const DISPATCH_CAPTURE_AND_REPLACE = 2;
81
82
    /**
83
     * Dispatch route output handling
84
     *
85
     * Capture all output and prepend it to the response body
86
     *
87
     * @type int
88
     */
89
    const DISPATCH_CAPTURE_AND_PREPEND = 3;
90
91
    /**
92
     * Dispatch route output handling
93
     *
94
     * Capture all output and append it to the response body
95
     *
96
     * @type int
97
     */
98
    const DISPATCH_CAPTURE_AND_APPEND = 4;
99
100
    /**
101
     * The types to detect in a defined match "block"
102
     *
103
     * Examples of these blocks are as follows:
104
     *
105
     * - integer:       '[i:id]'
106
     * - alphanumeric:  '[a:username]'
107
     * - hexadecimal:   '[h:color]'
108
     * - slug:          '[s:article]'
109
     *
110
     * @type array
111
     */
112
    protected $match_types = [
113
        'i'  => '[0-9]++',
114
        'a'  => '[0-9A-Za-z]++',
115
        'h'  => '[0-9A-Fa-f]++',
116
        's'  => '[0-9A-Za-z-_]++',
117
        '*'  => '.+?',
118
        '**' => '.++',
119
        ''   => '[^/]+?',
120
    ];
121
122
    /**
123
     * Collection of the routes to match on dispatch
124
     *
125
     * @type RouteCollection
126
     */
127
    protected $routes;
128
129
    /**
130
     * The Route factory object responsible for creating Route instances
131
     *
132
     * @type AbstractRouteFactory
133
     */
134
    protected $route_factory;
135
136
    /**
137
     * A stack of error callback callables
138
     *
139
     * @type SplStack
140
     */
141
    protected $error_callbacks;
142
143
    /**
144
     * A stack of HTTP error callback callables
145
     *
146
     * @type SplStack
147
     */
148
    protected $http_error_callbacks;
149
150
    /**
151
     * A queue of callbacks to call after processing the dispatch loop
152
     * and before the response is sent
153
     *
154
     * @type SplQueue
155
     */
156
    protected $after_filter_callbacks;
157
158
    /**
159
     * The output buffer level used by the dispatch process
160
     *
161
     * @type int
162
     */
163
    private $output_buffer_level;
164
165
    /**
166
     * The Request object passed to each matched route
167
     *
168
     * @type Request
169
     */
170
    protected $request;
171
172
    /**
173
     * The Response object passed to each matched route
174
     *
175
     * @type AbstractResponse
176
     */
177
    protected $response;
178
179
    /**
180
     * The service provider object passed to each matched route
181
     *
182
     * @type ServiceProvider
183
     */
184
    protected $service;
185
186
    /**
187
     * A generic variable passed to each matched route
188
     *
189
     * @type mixed
190
     */
191
    protected $app;
192
193
    /**
194
     * Constructor
195
     *
196
     * Create a new Klein instance with optionally injected dependencies
197
     * This DI allows for easy testing, object mocking, or class extension
198
     *
199
     * @param ServiceProvider $service              Service provider object responsible for utilitarian behaviors
200
     * @param mixed $app                            An object passed to each route callback, defaults to an App instance
201
     * @param RouteCollection $routes               Collection object responsible for containing all route instances
202
     * @param AbstractRouteFactory $route_factory   A factory class responsible for creating Route instances
203
     */
204
    public function __construct(
205
        ServiceProvider $service = null,
206
        $app = null,
207
        RouteCollection $routes = null,
208
        AbstractRouteFactory $route_factory = null
209
    ) {
210
        // Instantiate and fall back to defaults
211
        $this->service       = $service       ?: new ServiceProvider();
212
        $this->app           = $app           ?: new App();
213
        $this->routes        = $routes        ?: new RouteCollection();
214
        $this->route_factory = $route_factory ?: new RouteFactory();
215
216
        $this->error_callbacks        = new SplStack();
217
        $this->http_error_callbacks   = new SplStack();
218
        $this->after_filter_callbacks = new SplQueue();
219
    }
220
221
    /**
222
     * Returns the routes object
223
     *
224
     * @return RouteCollection
225
     */
226
    public function routes()
227
    {
228
        return $this->routes;
229
    }
230
231
    /**
232
     * Returns the request object
233
     *
234
     * @return Request
235
     */
236
    public function request()
237
    {
238
        return $this->request;
239
    }
240
241
    /**
242
     * Returns the response object
243
     *
244
     * @return Response
245
     */
246
    public function response()
247
    {
248
        return $this->response;
249
    }
250
251
    /**
252
     * Returns the service object
253
     *
254
     * @return ServiceProvider
255
     */
256
    public function service()
257
    {
258
        return $this->service;
259
    }
260
261
    /**
262
     * Returns the app object
263
     *
264
     * @return mixed
265
     */
266
    public function app()
267
    {
268
        return $this->app;
269
    }
270
271
    /**
272
     * Parse our extremely loose argument order of our "respond" method and its aliases
273
     *
274
     * This method takes its arguments in a loose format and order.
275
     * The method signature is simply there for documentation purposes, but allows
276
     * for the minimum of a callback to be passed in its current configuration.
277
     *
278
     * @see Routing::respond()
279
     * @param mixed $args               An argument array. Hint: This works well when passing "func_get_args()"
280
     *  @named string | array $method   HTTP Method to match
281
     *  @named string $path             Route URI path to match
282
     *  @named callable $callback       Callable callback method to execute on route match
283
     * @return array                    A named parameter array containing the keys: 'method', 'path', and 'callback'
284
     */
285
    protected function parseLooseArgumentOrder(array $args)
286
    {
287
        // Get the arguments in a very loose format
288
        $callback = array_pop($args);
289
        $path = array_pop($args);
290
        $method = array_pop($args);
291
292
        // Return a named parameter array
293
        return [
294
            'method' => $method,
295
            'path' => $path,
296
            'callback' => $callback,
297
        ];
298
    }
299
300
    /**
301
     * Add a new route to be matched on dispatch
302
     *
303
     * Essentially, this method is a standard "Route" builder/factory,
304
     * allowing a loose argument format and a standard way of creating
305
     * Route instances
306
     *
307
     * This method takes its arguments in a very loose format
308
     * The only "required" parameter is the callback (which is very strange considering the argument definition order)
309
     *
310
     * <code>
311
     * $router = new Klein();
312
     *
313
     * $router->respond( function() {
314
     *     echo 'this works';
315
     * });
316
     * $router->respond( '/endpoint', function() {
317
     *     echo 'this also works';
318
     * });
319
     * $router->respond( 'POST', '/endpoint', function() {
320
     *     echo 'this also works!!!!';
321
     * });
322
     * </code>
323
     *
324
     * @param string|array $method    HTTP Method to match
325
     * @param string $path              Route URI path to match
326
     * @param callable $callback        Callable callback method to execute on route match
327
     * @return Route
328
     */
329
    public function respond($method, $path = '*', $callback = null)
330
    {
331
        // Get the arguments in a very loose format
332
        extract(
333
            $this->parseLooseArgumentOrder(func_get_args()),
334
            EXTR_OVERWRITE
335
        );
336
337
        $route = $this->route_factory->build($callback, $path, $method);
338
339
        $this->routes->add($route);
340
341
        return $route;
342
    }
343
344
    /**
345
     * Collect a set of routes under a common namespace
346
     *
347
     * The routes may be passed in as either a callable (which holds the route definitions),
348
     * or as a string of a filename, of which to "include" under the Klein router scope
349
     *
350
     * <code>
351
     * $router = new Klein();
352
     *
353
     * $router->with('/users', function($router) {
354
     *     $router->respond( '/', function() {
355
     *         // do something interesting
356
     *     });
357
     *     $router->respond( '/[i:id]', function() {
358
     *         // do something different
359
     *     });
360
     * });
361
     *
362
     * $router->with('/cars', __DIR__ . '/routes/cars.php');
363
     * </code>
364
     *
365
     * @param string $namespace         The namespace under which to collect the routes
366
     * @param callable|string $routes   The defined routes callable or filename to collect under the namespace
367
     * @return void
368
     */
369
    public function with($namespace, $routes)
370
    {
371
        $previous = $this->route_factory->getNamespace();
372
373
        $this->route_factory->appendNamespace($namespace);
374
375
        if (is_callable($routes)) {
376
            if (is_string($routes)) {
377
                $routes($this);
378
            } else {
379
                call_user_func($routes, $this);
380
            }
381
        } else {
382
            require $routes;
383
        }
384
385
        $this->route_factory->setNamespace($previous);
386
    }
387
388
    /**
389
     * Dispatch the request to the appropriate route(s)
390
     *
391
     * Dispatch with optionally injected dependencies
392
     * This DI allows for easy testing, object mocking, or class extension
393
     *
394
     * @param Request          $request       The request object to give to each callback
395
     * @param AbstractResponse $response      The response object to give to each callback
396
     * @param boolean          $send_response Whether or not to "send" the response after the last route has been matched
397
     * @param int              $capture       Specify a DISPATCH_* constant to change the output capturing behavior
398
     * @return void|string
399
     * @throws Throwable
400
     */
401
    public function dispatch(
402
        Request          $request       = null,
403
        AbstractResponse $response      = null,
404
                         $send_response = true,
405
                         $capture       = self::DISPATCH_NO_CAPTURE
406
    ) {
407
        // Set/Initialize our objects to be sent in each callback
408
        $this->request  = $request  ?: Request::createFromGlobals();
409
        $this->response = $response ?: new Response();
410
411
        // Bind our objects to our service
412
        $this->service->bind($this->request, $this->response);
413
414
        // Prepare any named routes
415
        $this->routes->prepareNamed();
416
417
        // Grab some data from the request
418
        $uri        = $this->request->pathname();
419
        $req_method = $this->request->method();
420
421
        // Set up some variables for matching
422
        $skip_num        = 0;
423
        // Get a clone of the routes collection, as it may have been injected
424
        $matched         = $this->routes->cloneEmpty();
425
        $methods_matched = array();
426
        $params          = array();
427
        $apc             = function_exists('apc_fetch');
428
429
        // Start output buffering
430
        ob_start();
431
        $this->output_buffer_level = ob_get_level();
432
433
        try {
434
            foreach ($this->routes as $route) {
435
                // Are we skipping any matches?
436
                if ($skip_num > 0) {
437
                    $skip_num--;
438
                    continue;
439
                }
440
441
                // Grab the properties of the route handler
442
                $method      = $route->getMethod();
443
                $path        = $route->getPath();
444
                $count_match = $route->getCountMatch();
445
446
                // Keep track of whether this specific request method was matched
447
                $method_match = null;
448
449
                // Was a method specified? If so, check it against the current request method
450
                if (is_array($method)) {
451
                    foreach ($method as $test) {
452
                        if (strcasecmp($req_method, $test) === 0) {
453
                            $method_match = true;
454
                        } elseif (strcasecmp($req_method, 'HEAD') === 0
455
                              && (strcasecmp($test, 'HEAD') === 0 || strcasecmp($test, 'GET') === 0)) {
456
457
                            // Test for HEAD request (like GET)
458
                            $method_match = true;
459
                        }
460
                    }
461
462
                    if (null === $method_match) {
463
                        $method_match = false;
464
                    }
465
                } elseif (null !== $method && strcasecmp($req_method, $method) !== 0) {
466
                    $method_match = false;
467
468
                    // Test for HEAD request (like GET)
469
                    if (strcasecmp($req_method, 'HEAD') === 0
470
                        && (strcasecmp($method, 'HEAD') === 0
471
                            || strcasecmp($method, 'GET') === 0 )
472
                    ) {
473
474
                        $method_match = true;
475
                    }
476
                } elseif (null !== $method && strcasecmp($req_method, $method) === 0) {
477
                    $method_match = true;
478
                }
479
480
                // If the method was matched or if it wasn't even passed (in the route callback)
481
                $possible_match = (null === $method_match) || $method_match;
482
483
                // ! is used to negate a match
484
                if (isset($path[0]) && $path[0] === '!') {
485
                    $negate = true;
486
                    $i = 1;
487
                } else {
488
                    $negate = false;
489
                    $i = 0;
490
                }
491
492
                // Check for a wildcard (match all)
493
                if ($path === '*') {
494
                    $match = true;
495
496
                } elseif (($path === '404' && $matched->isEmpty() && count($methods_matched) <= 0)
497
                       || ($path === '405' && $matched->isEmpty() && count($methods_matched) > 0)) {
498
499
                    // Warn user of deprecation
500
                    trigger_error(
501
                        'Use of 404/405 "routes" is deprecated. Use $klein->onHttpError() instead.',
502
                        E_USER_DEPRECATED
503
                    );
504
                    // TODO: Possibly remove in future, here for backwards compatibility
505
                    $this->onHttpError($route);
506
507
                    continue;
508
509
                } elseif (isset($path[$i]) && $path[$i] === '@') {
510
                    // @ is used to specify custom regex
511
512
                    $match = preg_match('`' . substr($path, $i + 1) . '`', $uri, $params);
513
514
                } else {
515
                    // Compiling and matching regular expressions is relatively
516
                    // expensive, so try and match by a substring first
517
518
                    $expression = null;
519
                    $regex = false;
520
                    $j = 0;
521
                    $n = isset($path[$i]) ? $path[$i] : null;
522
523
                    // Find the longest non-regex substring and match it against the URI
524
                    while (true) {
525
                        if (!isset($path[$i])) {
526
                            break;
527
                        } elseif (false === $regex) {
528
                            $c = $n;
529
                            $regex = $c === '[' || $c === '(' || $c === '.';
530
                            if (false === $regex && false !== isset($path[$i+1])) {
531
                                $n = $path[$i + 1];
532
                                $regex = $n === '?' || $n === '+' || $n === '*' || $n === '{';
533
                            }
534
                            if (false === $regex && $c !== '/' && (!isset($uri[$j]) || $c !== $uri[$j])) {
535
                                continue 2;
536
                            }
537
                            $j++;
538
                        }
539
                        $expression .= $path[$i++];
540
                    }
541
542
                    try {
543
                        // Check if there's a cached regex string
544
                        if (false !== $apc) {
545
                            $regex = apc_fetch("route:$expression");
546
                            if (false === $regex) {
547
                                $regex = $this->compileRoute($expression);
548
                                apc_store("route:$expression", $regex);
549
                            }
550
                        } else {
551
                            $regex = $this->compileRoute($expression);
552
                        }
553
                    } catch (RegularExpressionCompilationException $e) {
554
                        throw RoutePathCompilationException::createFromRoute($route, $e);
555
                    }
556
557
                    $match = preg_match($regex, $uri, $params);
558
                }
559
560
                if (isset($match) && $match ^ $negate) {
561
                    if ($possible_match) {
562
                        if (!empty($params)) {
563
                            /**
564
                             * URL Decode the params according to RFC 3986
565
                             * @link http://www.faqs.org/rfcs/rfc3986
566
                             *
567
                             * Decode here AFTER matching as per @chriso's suggestion
568
                             * @link https://github.com/klein/klein.php/issues/117#issuecomment-21093915
569
                             */
570
                            $params = array_map('rawurldecode', $params);
571
572
                            $this->request->paramsNamed()->merge($params);
573
                        }
574
575
                        // Execute middleware before route Callback
576
                        $this->doMiddleware($route);
577
578
                        // Handle our response callback
579
                        try {
580
                            $this->handleRouteCallback($route, $matched, $methods_matched);
581
                        } catch (DispatchHaltedException $e) {
582
                            switch ($e->getCode()) {
583
                                case DispatchHaltedException::SKIP_THIS:
584
                                    continue 2;
585
                                    break;
586
                                case DispatchHaltedException::SKIP_NEXT:
587
                                    $skip_num = $e->getNumberOfSkips();
588
                                    break;
589
                                case DispatchHaltedException::SKIP_REMAINING:
590
                                    break 2;
591
                                default:
592
                                    throw $e;
593
                            }
594
                        }
595
596
                        if ($path !== '*') {
597
                            $count_match && $matched->add($route);
598
                        }
599
                    }
600
601
                    // Don't bother counting this as a method match if the route isn't supposed to match anyway
602
                    if ($count_match) {
603
                        // Keep track of possibly matched methods
604
                        $methods_matched = array_merge($methods_matched, (array) $method);
605
                        $methods_matched = array_filter($methods_matched);
606
                        $methods_matched = array_unique($methods_matched);
607
                    }
608
                }
609
            }
610
611
            // Handle our 404/405 conditions
612
            if ($matched->isEmpty() && count($methods_matched) > 0) {
613
                // Add our methods to our allow header
614
                $this->response->header('Allow', implode(', ', $methods_matched));
615
616
                if (strcasecmp($req_method, 'OPTIONS') !== 0) {
617
                    throw HttpException::createFromCode(405);
618
                }
619
            } elseif ($matched->isEmpty()) {
620
                throw HttpException::createFromCode(404);
621
            }
622
623
        } catch (HttpExceptionInterface $e) {
624
            // Grab our original response lock state
625
            $locked = $this->response->isLocked();
626
627
            // Call our http error handlers
628
            $this->httpError($e, $matched, $methods_matched);
629
630
            // Make sure we return our response to its original lock state
631
            if (!$locked) {
632
                $this->response->unlock();
633
            }
634
635
        } catch (Throwable $e) { // PHP 7 compatibility
636
            $this->error($e);
637
        } catch (Exception $e) { // TODO: Remove this catch block once PHP 5.x support is no longer necessary.
638
            $this->error($e);
639
        }
640
641
        try {
642
            if ($this->response->chunked) {
643
                $this->response->chunk();
644
645
            } else {
646
                // Output capturing behavior
647
                switch($capture) {
648
                    case self::DISPATCH_CAPTURE_AND_RETURN:
649
                        $buffed_content = null;
650
                        while (ob_get_level() >= $this->output_buffer_level) {
651
                            $buffed_content = ob_get_clean();
652
                        }
653
                        return $buffed_content;
654
                        break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
655
                    case self::DISPATCH_CAPTURE_AND_REPLACE:
656
                        while (ob_get_level() >= $this->output_buffer_level) {
657
                            $this->response->body(ob_get_clean());
658
                        }
659
                        break;
660
                    case self::DISPATCH_CAPTURE_AND_PREPEND:
661
                        while (ob_get_level() >= $this->output_buffer_level) {
662
                            $this->response->prepend(ob_get_clean());
663
                        }
664
                        break;
665
                    case self::DISPATCH_CAPTURE_AND_APPEND:
666
                        while (ob_get_level() >= $this->output_buffer_level) {
667
                            $this->response->append(ob_get_clean());
668
                        }
669
                        break;
670
                    default:
671
                        // If not a handled capture strategy, default to no capture
672
                        $capture = self::DISPATCH_NO_CAPTURE;
673
                }
674
            }
675
676
            // Test for HEAD request (like GET)
677
            if (strcasecmp($req_method, 'HEAD') === 0) {
678
                // HEAD requests shouldn't return a body
679
                $this->response->body('');
680
681
                while (ob_get_level() >= $this->output_buffer_level) {
682
                    ob_end_clean();
683
                }
684
            } elseif (self::DISPATCH_NO_CAPTURE === $capture) {
685
                while (ob_get_level() >= $this->output_buffer_level) {
686
                    ob_end_flush();
687
                }
688
            }
689
        } catch (LockedResponseException $e) {
690
            // Do nothing, since this is an automated behavior
691
        }
692
693
        // Run our after dispatch callbacks
694
        $this->callAfterDispatchCallbacks();
695
696
        if ($send_response && !$this->response->isSent()) {
697
            $this->response->send();
698
        }
699
    }
700
701
    /**
702
     * Execute middleware handle method
703
     *
704
     * @param Route $route
705
     * @throws Exceptions\MiddlewareNotFoundException
706
     */
707
    protected function doMiddleware(Route $route)
708
    {
709
        /** @var MiddlewareInterface $middleware */
710
        foreach($route->getMiddleware() as $middleware) {
711
            if (is_array($middleware)) {
712
                foreach ($middleware as $group) {
713
                    $group->handle($this->request());
714
                }
715
            } else {
716
                $middleware->handle($this->request());
717
            }
718
        }
719
    }
720
721
    /**
722
     * Compiles a route string to a regular expression
723
     *
724
     * @param string $route     The route string to compile
725
     * @return string
726
     */
727
    protected function compileRoute($route)
728
    {
729
        // First escape all of the non-named param (non [block]s) for regex-chars
730
        $route = preg_replace_callback(
731
            static::ROUTE_ESCAPE_REGEX,
732
            function ($match) {
733
                return preg_quote($match[0]);
734
            },
735
            $route
736
        );
737
738
        // Get a local reference of the match types to pass into our closure
739
        $match_types = $this->match_types;
740
741
        // Now let's actually compile the path
742
        $route = preg_replace_callback(
743
            static::ROUTE_COMPILE_REGEX,
744
            function ($match) use ($match_types) {
745
                list(, $pre, $type, $param, $optional) = $match;
746
747
                if (isset($match_types[$type])) {
748
                    $type = $match_types[$type];
749
                }
750
751
                // Older versions of PCRE require the 'P' in (?P<named>)
752
                $pattern = '(?:'
753
                    . ($pre !== '' ? $pre : null)
754
                    . '('
755
                    . ($param !== '' ? "?P<$param>" : null)
756
                    . $type
757
                    . '))'
758
                    . ($optional !== '' ? '?' : null);
759
760
                return $pattern;
761
            },
762
            $route
763
        );
764
765
        $regex = "`^$route$`";
766
767
        // Check if our regular expression is valid
768
        $this->validateRegularExpression($regex);
769
770
        return $regex;
771
    }
772
773
    /**
774
     * Validate a regular expression
775
     *
776
     * This simply checks if the regular expression is able to be compiled
777
     * and converts any warnings or notices in the compilation to an exception
778
     *
779
     * @param string $regex                          The regular expression to validate
780
     * @throws RegularExpressionCompilationException If the expression can't be compiled
781
     * @return boolean
782
     */
783
    private function validateRegularExpression($regex)
784
    {
785
        $error_string = null;
786
787
        // Set an error handler temporarily
788
        set_error_handler(
789
            function ($errno, $errstr) use (&$error_string) {
790
                $error_string = $errstr;
791
            },
792
            E_NOTICE | E_WARNING
793
        );
794
795
        if (false === preg_match($regex, null) || !empty($error_string)) {
796
            // Remove our temporary error handler
797
            restore_error_handler();
798
799
            throw new RegularExpressionCompilationException(
800
                $error_string,
801
                preg_last_error()
802
            );
803
        }
804
805
        // Remove our temporary error handler
806
        restore_error_handler();
807
808
        return true;
809
    }
810
811
    /**
812
     * Get the path for a given route
813
     *
814
     * This looks up the route by its passed name and returns
815
     * the path/url for that route, with its URL params as
816
     * placeholders unless you pass a valid key-value pair array
817
     * of the placeholder params and their values
818
     *
819
     * If a pathname is a complex/custom regular expression, this
820
     * method will simply return the regular expression used to
821
     * match the request pathname, unless an optional boolean is
822
     * passed "flatten_regex" which will flatten the regular
823
     * expression into a simple path string
824
     *
825
     * This method, and its style of reverse-compilation, was originally
826
     * inspired by a similar effort by Gilles Bouthenot (@gbouthenot)
827
     *
828
     * @link https://github.com/gbouthenot
829
     * @param string $route_name        The name of the route
830
     * @param array $params             The array of placeholder fillers
831
     * @param boolean $flatten_regex    Optionally flatten custom regular expressions to "/"
832
     * @throws OutOfBoundsException     If the route requested doesn't exist
833
     * @return string
834
     */
835
    public function getPathFor($route_name, array $params = null, $flatten_regex = true)
836
    {
837
        // First, grab the route
838
        $route = $this->routes->get($route_name);
839
840
        // Make sure we are getting a valid route
841
        if (null === $route) {
842
            throw new OutOfBoundsException('No such route with name: '. $route_name);
843
        }
844
845
        $path = $route->getPath();
846
847
        // Use our compilation regex to reverse the path's compilation from its definition
848
        $reversed_path = preg_replace_callback(
849
            static::ROUTE_COMPILE_REGEX,
850
            function ($match) use ($params) {
851
                list($block, $pre, , $param, $optional) = $match;
852
853
                if (isset($params[$param])) {
854
                    return $pre. $params[$param];
855
                } elseif ($optional) {
856
                    return '';
857
                }
858
859
                return $block;
860
            },
861
            $path
862
        );
863
864
        // If the path and reversed_path are the same, the regex must have not matched/replaced
865
        if ($path === $reversed_path && $flatten_regex && strpos($path, '@') === 0) {
866
            // If the path is a custom regular expression and we're "flattening", just return a slash
867
            $path = '/';
868
        } else {
869
            $path = $reversed_path;
870
        }
871
872
        return $path;
873
    }
874
875
    /**
876
     * Handle a route's callback
877
     *
878
     * This handles common exceptions and their output
879
     * to keep the "dispatch()" method DRY
880
     *
881
     * @param Route $route
882
     * @param RouteCollection $matched
883
     * @param array $methods_matched
884
     * @return void
885
     */
886
    protected function handleRouteCallback(Route $route, RouteCollection $matched, array $methods_matched)
887
    {
888
        $callback = $route->getCallback();
889
890
        $params = [
891
            $this->request,
892
            $this->response,
893
            $this->service,
894
            $this->app,
895
            // Pass the Klein instance
896
            $this,
897
            $matched,
898
            $methods_matched,
899
        ];
900
901
        if (is_string($callback)) {
902
            $returned = app($callback, $params);
903
        } else {
904
            // Handle the callback
905
            $returned = call_user_func(
906
                // Instead of relying on the slower "invoke" magic
907
                $callback,
908
                $this->request,
909
                $this->response,
910
                $this->service,
911
                $this->app,
912
                // Pass the Klein instance
913
                $this,
914
                $matched,
915
                $methods_matched
916
            );
917
        }
918
919
        if ($returned instanceof AbstractResponse) {
920
            $this->response = $returned;
921
        } else {
922
            // Otherwise, attempt to append the returned data
923
            try {
924
                $this->response->append($returned);
925
            } catch (LockedResponseException $e) {
926
                // Do nothing, since this is an automated behavior
927
            }
928
        }
929
    }
930
931
    /**
932
     * Adds an error callback to the stack of error handlers
933
     *
934
     * @param callable $callback            The callable function to execute in the error handling chain
935
     * @return void
936
     */
937
    public function onError($callback)
938
    {
939
        $this->error_callbacks->push($callback);
940
    }
941
942
    /**
943
     * Routes an exception through the error callbacks
944
     *
945
     * @param Exception|Throwable $err The exception that occurred
946
     * @return void
947
     * @throws Throwable
948
     * @throws UnhandledException      If the error/exception isn't handled by an error callback
949
     */
950
    protected function error(Throwable $err)
951
    {
952
        $type = get_class($err);
953
        $msg = $err->getMessage();
954
955
        try {
956
            if (!$this->error_callbacks->isEmpty()) {
957
                foreach ($this->error_callbacks as $callback) {
958
                    if (is_callable($callback)) {
959
                        if (is_string($callback)) {
960
                            $callback($this, $msg, $type, $err);
961
962
                            return;
963
                        } else {
964
                            call_user_func($callback, $this, $msg, $type, $err);
965
966
                            return;
967
                        }
968
                    } else {
969
                        if (null !== $this->service && null !== $this->response) {
970
                            $this->service->flash($err);
971
                            $this->response->redirect($callback);
972
                        }
973
                    }
974
                }
975
            } else {
976
                $this->response->code(500);
977
978
                while (ob_get_level() >= $this->output_buffer_level) {
979
                    ob_end_clean();
980
                }
981
982
                throw $err;
983
            }
984
        } catch (Throwable $e) { // PHP 7 compatibility
985
            // Make sure to clean the output buffer before bailing
986
            while (ob_get_level() >= $this->output_buffer_level) {
987
                ob_end_clean();
988
            }
989
990
            throw $e;
991
        }
992
993
        // Lock our response, since we probably don't want
994
        // anything else messing with our error code/body
995
        $this->response->lock();
996
    }
997
998
    /**
999
     * Adds an HTTP error callback to the stack of HTTP error handlers
1000
     *
1001
     * @param callable $callback            The callable function to execute in the error handling chain
1002
     * @return void
1003
     */
1004
    public function onHttpError($callback)
1005
    {
1006
        $this->http_error_callbacks->push($callback);
1007
    }
1008
1009
    /**
1010
     * Handles an HTTP error exception through our HTTP error callbacks
1011
     *
1012
     * @param HttpExceptionInterface $http_exception    The exception that occurred
1013
     * @param RouteCollection $matched                  The collection of routes that were matched in dispatch
1014
     * @param array $methods_matched                    The HTTP methods that were matched in dispatch
1015
     * @return void
1016
     */
1017
    protected function httpError(HttpExceptionInterface $http_exception, RouteCollection $matched, $methods_matched)
1018
    {
1019
        if (!$this->response->isLocked()) {
1020
            $this->response->code($http_exception->getCode());
0 ignored issues
show
Bug introduced by
The method getCode() does not exist on app\framework\Component\...\HttpExceptionInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to app\framework\Component\...\HttpExceptionInterface. ( Ignorable by Annotation )

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

1020
            $this->response->code($http_exception->/** @scrutinizer ignore-call */ getCode());
Loading history...
1021
        }
1022
1023
        if (!$this->http_error_callbacks->isEmpty()) {
1024
            foreach ($this->http_error_callbacks as $callback) {
1025
                if ($callback instanceof Route) {
1026
                    $this->handleRouteCallback($callback, $matched, $methods_matched);
1027
                } elseif (is_callable($callback)) {
1028
                    if (is_string($callback)) {
1029
                        $callback(
1030
                            $http_exception->getCode(),
1031
                            $this,
1032
                            $matched,
1033
                            $methods_matched,
1034
                            $http_exception
1035
                        );
1036
                    } else {
1037
                        call_user_func(
1038
                            $callback,
1039
                            $http_exception->getCode(),
1040
                            $this,
1041
                            $matched,
1042
                            $methods_matched,
1043
                            $http_exception
1044
                        );
1045
                    }
1046
                }
1047
            }
1048
        }
1049
1050
        // Lock our response, since we probably don't want
1051
        // anything else messing with our error code/body
1052
        $this->response->lock();
1053
    }
1054
1055
    /**
1056
     * Adds a callback to the stack of handlers to run after the dispatch
1057
     * loop has handled all of the route callbacks and before the response
1058
     * is sent
1059
     *
1060
     * @param callable $callback            The callable function to execute in the after route chain
1061
     * @return void
1062
     */
1063
    public function afterDispatch($callback)
1064
    {
1065
        $this->after_filter_callbacks->enqueue($callback);
1066
    }
1067
1068
    /**
1069
     * Runs through and executes the after dispatch callbacks
1070
     *
1071
     * @return void
1072
     */
1073
    protected function callAfterDispatchCallbacks()
1074
    {
1075
        try {
1076
            foreach ($this->after_filter_callbacks as $callback) {
1077
                if (is_callable($callback)) {
1078
                    if (is_string($callback)) {
1079
                        $callback($this);
1080
1081
                    } else {
1082
                        call_user_func($callback, $this);
1083
1084
                    }
1085
                }
1086
            }
1087
        } catch (Throwable $e) { // PHP 7 compatibility
1088
            $this->error($e);
1089
        } catch (Exception $e) { // TODO: Remove this catch block once PHP 5.x support is no longer necessary.
1090
            $this->error($e);
1091
        }
1092
    }
1093
1094
    /**
1095
     * Quick alias to skip the current callback/route method from executing
1096
     *
1097
     * @throws DispatchHaltedException To halt/skip the current dispatch loop
1098
     * @return void
1099
     */
1100
    public function skipThis()
1101
    {
1102
        throw new DispatchHaltedException(null, DispatchHaltedException::SKIP_THIS);
1103
    }
1104
1105
    /**
1106
     * Quick alias to skip the next callback/route method from executing
1107
     *
1108
     * @param int $num The number of next matches to skip
1109
     * @throws DispatchHaltedException To halt/skip the current dispatch loop
1110
     * @return void
1111
     */
1112
    public function skipNext($num = 1)
1113
    {
1114
        $skip = new DispatchHaltedException(null, DispatchHaltedException::SKIP_NEXT);
1115
        $skip->setNumberOfSkips($num);
1116
1117
        throw $skip;
1118
    }
1119
1120
    /**
1121
     * Quick alias to stop the remaining callbacks/route methods from executing
1122
     *
1123
     * @throws DispatchHaltedException To halt/skip the current dispatch loop
1124
     * @return void
1125
     */
1126
    public function skipRemaining()
1127
    {
1128
        throw new DispatchHaltedException(null, DispatchHaltedException::SKIP_REMAINING);
1129
    }
1130
1131
    /**
1132
     * Alias to set a response code, lock the response, and halt the route matching/dispatching
1133
     *
1134
     * @param int $code     Optional HTTP status code to send
1135
     * @throws DispatchHaltedException To halt/skip the current dispatch loop
1136
     * @return void
1137
     */
1138
    public function abort($code = null)
1139
    {
1140
        if (null !== $code) {
1141
            throw HttpException::createFromCode($code);
1142
        }
1143
1144
        throw new DispatchHaltedException();
1145
    }
1146
1147
    /**
1148
     * OPTIONS alias for "respond()"
1149
     *
1150
     * @see Routing::respond()
1151
     * @param string $path
1152
     * @param callable $callback
1153
     * @return Route
1154
     */
1155
    public function options($path = '*', $callback = null)
1156
    {
1157
        // Options the arguments in a very loose format
1158
        extract(
1159
            $this->parseLooseArgumentOrder(func_get_args()),
1160
            EXTR_OVERWRITE
1161
        );
1162
1163
        return $this->respond('OPTIONS', $path, $callback);
1164
    }
1165
1166
    /**
1167
     * HEAD alias for "respond()"
1168
     *
1169
     * @see Routing::respond()
1170
     * @param string $path
1171
     * @param callable $callback
1172
     * @return Route
1173
     */
1174
    public function head($path = '*', $callback = null)
1175
    {
1176
        // Get the arguments in a very loose format
1177
        extract(
1178
            $this->parseLooseArgumentOrder(func_get_args()),
1179
            EXTR_OVERWRITE
1180
        );
1181
1182
        return $this->respond('HEAD', $path, $callback);
1183
    }
1184
1185
    /**
1186
     * GET alias for "respond()"
1187
     *
1188
     * @see Routing::respond()
1189
     * @param string $path
1190
     * @param callable $callback
1191
     * @return Route
1192
     */
1193
    public static function get($path = '*', $callback = null)
1194
    {
1195
        // Get the arguments in a very loose format
1196
        extract(
1197
            Router::getInstance()->parseLooseArgumentOrder(func_get_args()),
1198
            EXTR_OVERWRITE
1199
        );
1200
1201
        return Router::getInstance()->respond('GET', $path, $callback);
1202
    }
1203
1204
    /**
1205
     * POST alias for "respond()"
1206
     *
1207
     * @see Routing::respond()
1208
     * @param string $path
1209
     * @param callable $callback
1210
     * @return Route
1211
     */
1212
    public static function post($path = '*', $callback = null)
1213
    {
1214
        // Get the arguments in a very loose format
1215
        extract(
1216
            Router::getInstance()->parseLooseArgumentOrder(func_get_args()),
1217
            EXTR_OVERWRITE
1218
        );
1219
1220
        return Router::getInstance()->respond('POST', $path, $callback);
1221
    }
1222
1223
    /**
1224
     * PUT alias for "respond()"
1225
     *
1226
     * @see Routing::respond()
1227
     * @param string $path
1228
     * @param callable $callback
1229
     * @return Route
1230
     */
1231
    public static function put($path = '*', $callback = null)
1232
    {
1233
        // Get the arguments in a very loose format
1234
        extract(
1235
            Router::getInstance()->parseLooseArgumentOrder(func_get_args()),
1236
            EXTR_OVERWRITE
1237
        );
1238
1239
        return Router::getInstance()->respond('PUT', $path, $callback);
1240
    }
1241
1242
    /**
1243
     * DELETE alias for "respond()"
1244
     *
1245
     * @see Routing::respond()
1246
     * @param string $path
1247
     * @param callable $callback
1248
     * @return Route
1249
     */
1250
    public static function delete($path = '*', $callback = null)
1251
    {
1252
        // Get the arguments in a very loose format
1253
        extract(
1254
            Router::getInstance()->parseLooseArgumentOrder(func_get_args()),
1255
            EXTR_OVERWRITE
1256
        );
1257
1258
        return Router::getInstance()->respond('DELETE', $path, $callback);
1259
    }
1260
1261
    /**
1262
     * PATCH alias for "respond()"
1263
     *
1264
     * PATCH was added to HTTP/1.1 in RFC5789
1265
     *
1266
     * @link http://tools.ietf.org/html/rfc5789
1267
     * @see Routing::respond()
1268
     * @param string $path
1269
     * @param callable $callback
1270
     * @return Route
1271
     */
1272
    public static function patch($path = '*', $callback = null)
1273
    {
1274
        // Get the arguments in a very loose format
1275
        extract(
1276
            Router::getInstance()->parseLooseArgumentOrder(func_get_args()),
1277
            EXTR_OVERWRITE
1278
        );
1279
1280
        return Router::getInstance()->respond('PATCH', $path, $callback);
1281
    }
1282
1283
    /**
1284
     * Holding all the Auth routes
1285
     */
1286
    public static function auth(): void
1287
    {
1288
        static::get("/login", "Auth\AuthController@getLogin");
1289
        static::post("/login", "Auth\AuthController@postLogin");
1290
        static::get("/login_check", "Auth\AuthController@check");
1291
        static::get("/logout", "Auth\AuthController@logout");
1292
        static::get("/register", "Auth\AuthController@getRegister");
1293
        static::post("/register", "Auth\AuthController@postRegister");
1294
        static::get("/reset-password", "Auth\AuthController@getPasswordReset");
1295
        static::post("/reset-password/[:token]", "Auth\AuthController@resetPassword");
1296
        static::get("/reset-password/[:token]", "Auth\AuthController@resetPassword");
1297
    }
1298
}
1299