Completed
Push — master ( 93f6e0...a7ea34 )
by Aydin
27:25 queued 18:55
created

Klein::error()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 33
rs 6.7273
cc 7
eloc 20
nc 6
nop 1
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/chriso/klein.php
9
 * @license     MIT
10
 */
11
12
namespace Klein;
13
14
use Exception;
15
use Klein\DataCollection\RouteCollection;
16
use Klein\Exceptions\DispatchHaltedException;
17
use Klein\Exceptions\HttpException;
18
use Klein\Exceptions\HttpExceptionInterface;
19
use Klein\Exceptions\LockedResponseException;
20
use Klein\Exceptions\RegularExpressionCompilationException;
21
use Klein\Exceptions\RoutePathCompilationException;
22
use Klein\Exceptions\UnhandledException;
23
use OutOfBoundsException;
24
25
/**
26
 * Klein
27
 *
28
 * Main Klein router class
29
 */
30
class Klein
31
{
32
33
    /**
34
     * Class constants
35
     */
36
37
    /**
38
     * The regular expression used to compile and match URL's
39
     *
40
     * @type string
41
     */
42
    const ROUTE_COMPILE_REGEX = '`(\\\?(?:/|\.|))(?:\[([^:\]]*)(?::([^:\]]*))?\])(\?|)`';
43
44
    /**
45
     * The regular expression used to escape the non-named param section of a route URL
46
     *
47
     * @type string
48
     */
49
    const ROUTE_ESCAPE_REGEX = '`(?<=^|\])[^\]\[\?]+?(?=\[|$)`';
50
51
    /**
52
     * Dispatch route output handling
53
     *
54
     * Don't capture anything. Behave as normal.
55
     *
56
     * @type int
57
     */
58
    const DISPATCH_NO_CAPTURE = 0;
59
60
    /**
61
     * Dispatch route output handling
62
     *
63
     * Capture all output and return it from dispatch
64
     *
65
     * @type int
66
     */
67
    const DISPATCH_CAPTURE_AND_RETURN = 1;
68
69
    /**
70
     * Dispatch route output handling
71
     *
72
     * Capture all output and replace the response body with it
73
     *
74
     * @type int
75
     */
76
    const DISPATCH_CAPTURE_AND_REPLACE = 2;
77
78
    /**
79
     * Dispatch route output handling
80
     *
81
     * Capture all output and prepend it to the response body
82
     *
83
     * @type int
84
     */
85
    const DISPATCH_CAPTURE_AND_PREPEND = 3;
86
87
    /**
88
     * Dispatch route output handling
89
     *
90
     * Capture all output and append it to the response body
91
     *
92
     * @type int
93
     */
94
    const DISPATCH_CAPTURE_AND_APPEND = 4;
95
96
97
    /**
98
     * Class properties
99
     */
100
101
    /**
102
     * The types to detect in a defined match "block"
103
     *
104
     * Examples of these blocks are as follows:
105
     *
106
     * - integer:       '[i:id]'
107
     * - alphanumeric:  '[a:username]'
108
     * - hexadecimal:   '[h:color]'
109
     * - slug:          '[s:article]'
110
     *
111
     * @type array
112
     */
113
    protected $match_types = array(
114
        'i'  => '[0-9]++',
115
        'a'  => '[0-9A-Za-z]++',
116
        'h'  => '[0-9A-Fa-f]++',
117
        's'  => '[0-9A-Za-z-_]++',
118
        '*'  => '.+?',
119
        '**' => '.++',
120
        ''   => '[^/]+?'
121
    );
122
123
    /**
124
     * Collection of the routes to match on dispatch
125
     *
126
     * @type RouteCollection
127
     */
128
    protected $routes;
129
130
    /**
131
     * The Route factory object responsible for creating Route instances
132
     *
133
     * @type AbstractRouteFactory
134
     */
135
    protected $route_factory;
136
137
    /**
138
     * An array of error callback callables
139
     *
140
     * @type array[callable]
141
     */
142
    protected $errorCallbacks = array();
143
144
    /**
145
     * An array of HTTP error callback callables
146
     *
147
     * @type array[callable]
148
     */
149
    protected $httpErrorCallbacks = array();
150
151
    /**
152
     * An array of callbacks to call after processing the dispatch loop
153
     * and before the response is sent
154
     *
155
     * @type array[callable]
156
     */
157
    protected $afterFilterCallbacks = array();
158
159
160
    /**
161
     * Route objects
162
     */
163
164
    /**
165
     * The Request object passed to each matched route
166
     *
167
     * @type Request
168
     */
169
    protected $request;
170
171
    /**
172
     * The Response object passed to each matched route
173
     *
174
     * @type AbstractResponse
175
     */
176
    protected $response;
177
178
    /**
179
     * The service provider object passed to each matched route
180
     *
181
     * @type ServiceProvider
182
     */
183
    protected $service;
184
185
    /**
186
     * A generic variable passed to each matched route
187
     *
188
     * @type mixed
189
     */
190
    protected $app;
191
192
193
    /**
194
     * Methods
195
     */
196
197
    /**
198
     * Constructor
199
     *
200
     * Create a new Klein instance with optionally injected dependencies
201
     * This DI allows for easy testing, object mocking, or class extension
202
     *
203
     * @param ServiceProvider $service              Service provider object responsible for utilitarian behaviors
204
     * @param mixed $app                            An object passed to each route callback, defaults to an App instance
205
     * @param RouteCollection $routes               Collection object responsible for containing all route instances
206
     * @param AbstractRouteFactory $route_factory   A factory class responsible for creating Route instances
207
     */
208
    public function __construct(
209
        ServiceProvider $service = null,
210
        $app = null,
211
        RouteCollection $routes = null,
212
        AbstractRouteFactory $route_factory = null
213
    ) {
214
        // Instanciate and fall back to defaults
215
        $this->service       = $service       ?: new ServiceProvider();
216
        $this->app           = $app           ?: new App();
217
        $this->routes        = $routes        ?: new RouteCollection();
218
        $this->route_factory = $route_factory ?: new RouteFactory();
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 Klein::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 array(
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()),
0 ignored issues
show
Bug introduced by
$this->parseLooseArgumentOrder(func_get_args()) cannot be passed to extract() as the parameter $var_array expects a reference.
Loading history...
334
            EXTR_OVERWRITE
335
        );
336
337
        $route = $this->route_factory->build($callback, $path, $method);
0 ignored issues
show
Bug introduced by
It seems like $callback defined by parameter $callback on line 329 can also be of type null; however, Klein\AbstractRouteFactory::build() does only seem to accept callable, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
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
     */
400
    public function dispatch(
401
        Request $request = null,
402
        AbstractResponse $response = null,
403
        $send_response = true,
404
        $capture = self::DISPATCH_NO_CAPTURE
405
    ) {
406
        // Set/Initialize our objects to be sent in each callback
407
        $this->request = $request ?: Request::createFromGlobals();
408
        $this->response = $response ?: new Response();
409
410
        // Bind our objects to our service
411
        $this->service->bind($this->request, $this->response);
412
413
        // Prepare any named routes
414
        $this->routes->prepareNamed();
415
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
        $matched = $this->routes->cloneEmpty(); // Get a clone of the routes collection, as it may have been injected
424
        $methods_matched = array();
425
        $params = array();
426
        $apc = function_exists('apc_fetch');
427
428
        ob_start();
429
430
        try {
431
            foreach ($this->routes as $route) {
432
                // Are we skipping any matches?
433
                if ($skip_num > 0) {
434
                    $skip_num--;
435
                    continue;
436
                }
437
438
                // Grab the properties of the route handler
439
                $method = $route->getMethod();
440
                $path = $route->getPath();
441
                $count_match = $route->getCountMatch();
442
443
                // Keep track of whether this specific request method was matched
444
                $method_match = null;
445
446
                // Was a method specified? If so, check it against the current request method
447
                if (is_array($method)) {
448
                    foreach ($method as $test) {
449 View Code Duplication
                        if (strcasecmp($req_method, $test) === 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
450
                            $method_match = true;
451
                        } elseif (strcasecmp($req_method, 'HEAD') === 0
452
                              && (strcasecmp($test, 'HEAD') === 0 || strcasecmp($test, 'GET') === 0)) {
453
454
                            // Test for HEAD request (like GET)
455
                            $method_match = true;
456
                        }
457
                    }
458
459
                    if (null === $method_match) {
460
                        $method_match = false;
461
                    }
462 View Code Duplication
                } elseif (null !== $method && strcasecmp($req_method, $method) !== 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
463
                    $method_match = false;
464
465
                    // Test for HEAD request (like GET)
466
                    if (strcasecmp($req_method, 'HEAD') === 0
467
                        && (strcasecmp($method, 'HEAD') === 0 || strcasecmp($method, 'GET') === 0 )) {
468
469
                        $method_match = true;
470
                    }
471
                } elseif (null !== $method && strcasecmp($req_method, $method) === 0) {
472
                    $method_match = true;
473
                }
474
475
                // If the method was matched or if it wasn't even passed (in the route callback)
476
                $possible_match = (null === $method_match) || $method_match;
477
478
                // ! is used to negate a match
479
                if (isset($path[0]) && $path[0] === '!') {
480
                    $negate = true;
481
                    $i = 1;
482
                } else {
483
                    $negate = false;
484
                    $i = 0;
485
                }
486
487
                // Check for a wildcard (match all)
488
                if ($path === '*') {
489
                    $match = true;
490
491
                } elseif (($path === '404' && $matched->isEmpty() && count($methods_matched) <= 0)
492
                       || ($path === '405' && $matched->isEmpty() && count($methods_matched) > 0)) {
493
494
                    // Warn user of deprecation
495
                    trigger_error(
496
                        'Use of 404/405 "routes" is deprecated. Use $klein->onHttpError() instead.',
497
                        E_USER_DEPRECATED
498
                    );
499
                    // TODO: Possibly remove in future, here for backwards compatibility
500
                    $this->onHttpError($route);
501
502
                    continue;
503
504
                } elseif (isset($path[$i]) && $path[$i] === '@') {
505
                    // @ is used to specify custom regex
506
507
                    $match = preg_match('`' . substr($path, $i + 1) . '`', $uri, $params);
508
509
                } else {
510
                    // Compiling and matching regular expressions is relatively
511
                    // expensive, so try and match by a substring first
512
513
                    $expression = null;
514
                    $regex = false;
515
                    $j = 0;
516
                    $n = isset($path[$i]) ? $path[$i] : null;
517
518
                    // Find the longest non-regex substring and match it against the URI
519
                    while (true) {
520
                        if (!isset($path[$i])) {
521
                            break;
522
                        } elseif (false === $regex) {
523
                            $c = $n;
524
                            $regex = $c === '[' || $c === '(' || $c === '.';
525
                            if (false === $regex && false !== isset($path[$i+1])) {
526
                                $n = $path[$i + 1];
527
                                $regex = $n === '?' || $n === '+' || $n === '*' || $n === '{';
528
                            }
529
                            if (false === $regex && $c !== '/' && (!isset($uri[$j]) || $c !== $uri[$j])) {
530
                                continue 2;
531
                            }
532
                            $j++;
533
                        }
534
                        $expression .= $path[$i++];
535
                    }
536
537
                    try {
538
                        // Check if there's a cached regex string
539
                        if (false !== $apc) {
540
                            $regex = apc_fetch("route:$expression");
541
                            if (false === $regex) {
542
                                $regex = $this->compileRoute($expression);
543
                                apc_store("route:$expression", $regex);
0 ignored issues
show
Unused Code introduced by
The call to the function apc_store() seems unnecessary as the function has no side-effects.
Loading history...
544
                            }
545
                        } else {
546
                            $regex = $this->compileRoute($expression);
547
                        }
548
                    } catch (RegularExpressionCompilationException $e) {
549
                        throw RoutePathCompilationException::createFromRoute($route, $e);
550
                    }
551
552
                    $match = preg_match($regex, $uri, $params);
553
                }
554
555
                if (isset($match) && $match ^ $negate) {
556
                    if ($possible_match) {
557
                        if (!empty($params)) {
558
                            /**
559
                             * URL Decode the params according to RFC 3986
560
                             * @link http://www.faqs.org/rfcs/rfc3986
561
                             *
562
                             * Decode here AFTER matching as per @chriso's suggestion
563
                             * @link https://github.com/chriso/klein.php/issues/117#issuecomment-21093915
564
                             */
565
                            $params = array_map('rawurldecode', $params);
566
567
                            $this->request->paramsNamed()->merge($params);
568
                        }
569
570
                        // Handle our response callback
571
                        try {
572
                            $this->handleRouteCallback($route, $matched, $methods_matched);
0 ignored issues
show
Compatibility introduced by
$matched of type object<Klein\DataCollection\DataCollection> is not a sub-type of object<Klein\DataCollection\RouteCollection>. It seems like you assume a child class of the class Klein\DataCollection\DataCollection to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
573
574
                        } catch (DispatchHaltedException $e) {
575
                            switch ($e->getCode()) {
576
                                case DispatchHaltedException::SKIP_THIS:
577
                                    continue 2;
578
                                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
579
                                case DispatchHaltedException::SKIP_NEXT:
580
                                    $skip_num = $e->getNumberOfSkips();
581
                                    break;
582
                                case DispatchHaltedException::SKIP_REMAINING:
583
                                    break 2;
584
                                default:
585
                                    throw $e;
586
                            }
587
                        }
588
589
                        if ($path !== '*') {
590
                            $count_match && $matched->add($route);
591
                        }
592
                    }
593
594
                    // Don't bother counting this as a method match if the route isn't supposed to match anyway
595
                    if ($count_match) {
596
                        // Keep track of possibly matched methods
597
                        $methods_matched = array_merge($methods_matched, (array) $method);
598
                        $methods_matched = array_filter($methods_matched);
599
                        $methods_matched = array_unique($methods_matched);
600
                    }
601
                }
602
            }
603
604
            // Handle our 404/405 conditions
605
            if ($matched->isEmpty() && count($methods_matched) > 0) {
606
                // Add our methods to our allow header
607
                $this->response->header('Allow', implode(', ', $methods_matched));
608
609
                if (strcasecmp($req_method, 'OPTIONS') !== 0) {
610
                    throw HttpException::createFromCode(405);
611
                }
612
            } elseif ($matched->isEmpty()) {
613
                throw HttpException::createFromCode(404);
614
            }
615
616
        } catch (HttpExceptionInterface $e) {
617
            // Grab our original response lock state
618
            $locked = $this->response->isLocked();
619
620
            // Call our http error handlers
621
            $this->httpError($e, $matched, $methods_matched);
0 ignored issues
show
Compatibility introduced by
$matched of type object<Klein\DataCollection\DataCollection> is not a sub-type of object<Klein\DataCollection\RouteCollection>. It seems like you assume a child class of the class Klein\DataCollection\DataCollection to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
622
623
            // Make sure we return our response to its original lock state
624
            if (!$locked) {
625
                $this->response->unlock();
626
            }
627
628
        } catch (Exception $e) {
629
            $this->error($e);
630
        }
631
632
        try {
633
            if ($this->response->chunked) {
634
                $this->response->chunk();
635
636
            } else {
637
                // Output capturing behavior
638
                switch($capture) {
639
                    case self::DISPATCH_CAPTURE_AND_RETURN:
640
                        $buffed_content = null;
641
                        if (ob_get_level()) {
642
                            $buffed_content = ob_get_clean();
643
                        }
644
                        return $buffed_content;
645
                        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...
646
                    case self::DISPATCH_CAPTURE_AND_REPLACE:
647
                        if (ob_get_level()) {
648
                            $this->response->body(ob_get_clean());
649
                        }
650
                        break;
651
                    case self::DISPATCH_CAPTURE_AND_PREPEND:
652
                        if (ob_get_level()) {
653
                            $this->response->prepend(ob_get_clean());
654
                        }
655
                        break;
656
                    case self::DISPATCH_CAPTURE_AND_APPEND:
657
                        if (ob_get_level()) {
658
                            $this->response->append(ob_get_clean());
659
                        }
660
                        break;
661
                    case self::DISPATCH_NO_CAPTURE:
662
                    default:
663
                        if (ob_get_level()) {
664
                            ob_end_flush();
665
                        }
666
                }
667
            }
668
669
            // Test for HEAD request (like GET)
670
            if (strcasecmp($req_method, 'HEAD') === 0) {
671
                // HEAD requests shouldn't return a body
672
                $this->response->body('');
673
674
                if (ob_get_level()) {
675
                    ob_clean();
676
                }
677
            }
678
        } catch (LockedResponseException $e) {
679
            // Do nothing, since this is an automated behavior
680
        }
681
682
        // Run our after dispatch callbacks
683
        $this->callAfterDispatchCallbacks();
684
685
        if ($send_response && !$this->response->isSent()) {
686
            $this->response->send();
687
        }
688
    }
689
690
    /**
691
     * Compiles a route string to a regular expression
692
     *
693
     * @param string $route     The route string to compile
694
     * @return string
695
     */
696
    protected function compileRoute($route)
697
    {
698
        // First escape all of the non-named param (non [block]s) for regex-chars
699
        $route = preg_replace_callback(
700
            static::ROUTE_ESCAPE_REGEX,
701
            function ($match) {
702
                return preg_quote($match[0]);
703
            },
704
            $route
705
        );
706
707
        // Get a local reference of the match types to pass into our closure
708
        $match_types = $this->match_types;
709
710
        // Now let's actually compile the path
711
        $route = preg_replace_callback(
712
            static::ROUTE_COMPILE_REGEX,
713
            function ($match) use ($match_types) {
714
                list(, $pre, $type, $param, $optional) = $match;
715
716
                if (isset($match_types[$type])) {
717
                    $type = $match_types[$type];
718
                }
719
720
                // Older versions of PCRE require the 'P' in (?P<named>)
721
                $pattern = '(?:'
722
                         . ($pre !== '' ? $pre : null)
723
                         . '('
724
                         . ($param !== '' ? "?P<$param>" : null)
725
                         . $type
726
                         . '))'
727
                         . ($optional !== '' ? '?' : null);
728
729
                return $pattern;
730
            },
731
            $route
732
        );
733
734
        $regex = "`^$route$`";
735
736
        // Check if our regular expression is valid
737
        $this->validateRegularExpression($regex);
738
739
        return $regex;
740
    }
741
742
    /**
743
     * Validate a regular expression
744
     *
745
     * This simply checks if the regular expression is able to be compiled
746
     * and converts any warnings or notices in the compilation to an exception
747
     *
748
     * @param string $regex                          The regular expression to validate
749
     * @throws RegularExpressionCompilationException If the expression can't be compiled
750
     * @return boolean
751
     */
752
    private function validateRegularExpression($regex)
753
    {
754
        $error_string = null;
755
756
        // Set an error handler temporarily
757
        set_error_handler(
758
            function ($errno, $errstr) use (&$error_string) {
759
                $error_string = $errstr;
760
            },
761
            E_NOTICE | E_WARNING
762
        );
763
764
        if (false === preg_match($regex, null) || !empty($error_string)) {
765
            // Remove our temporary error handler
766
            restore_error_handler();
767
768
            throw new RegularExpressionCompilationException(
769
                $error_string,
770
                preg_last_error()
771
            );
772
        }
773
774
        // Remove our temporary error handler
775
        restore_error_handler();
776
777
        return true;
778
    }
779
780
    /**
781
     * Get the path for a given route
782
     *
783
     * This looks up the route by its passed name and returns
784
     * the path/url for that route, with its URL params as
785
     * placeholders unless you pass a valid key-value pair array
786
     * of the placeholder params and their values
787
     *
788
     * If a pathname is a complex/custom regular expression, this
789
     * method will simply return the regular expression used to
790
     * match the request pathname, unless an optional boolean is
791
     * passed "flatten_regex" which will flatten the regular
792
     * expression into a simple path string
793
     *
794
     * This method, and its style of reverse-compilation, was originally
795
     * inspired by a similar effort by Gilles Bouthenot (@gbouthenot)
796
     *
797
     * @link https://github.com/gbouthenot
798
     * @param string $route_name        The name of the route
799
     * @param array $params             The array of placeholder fillers
800
     * @param boolean $flatten_regex    Optionally flatten custom regular expressions to "/"
801
     * @throws OutOfBoundsException     If the route requested doesn't exist
802
     * @return string
803
     */
804
    public function getPathFor($route_name, array $params = null, $flatten_regex = true)
805
    {
806
        // First, grab the route
807
        $route = $this->routes->get($route_name);
808
809
        // Make sure we are getting a valid route
810
        if (null === $route) {
811
            throw new OutOfBoundsException('No such route with name: '. $route_name);
812
        }
813
814
        $path = $route->getPath();
815
816
        // Use our compilation regex to reverse the path's compilation from its definition
817
        $reversed_path = preg_replace_callback(
818
            static::ROUTE_COMPILE_REGEX,
819
            function ($match) use ($params) {
820
                list($block, $pre, , $param, $optional) = $match;
821
822
                if (isset($params[$param])) {
823
                    return $pre. $params[$param];
824
                } elseif ($optional) {
825
                    return '';
826
                }
827
828
                return $block;
829
            },
830
            $path
831
        );
832
833
        // If the path and reversed_path are the same, the regex must have not matched/replaced
834
        if ($path === $reversed_path && $flatten_regex && strpos($path, '@') === 0) {
835
            // If the path is a custom regular expression and we're "flattening", just return a slash
836
            $path = '/';
837
        } else {
838
            $path = $reversed_path;
839
        }
840
841
        return $path;
842
    }
843
844
    /**
845
     * Handle a route's callback
846
     *
847
     * This handles common exceptions and their output
848
     * to keep the "dispatch()" method DRY
849
     *
850
     * @param Route $route
851
     * @param RouteCollection $matched
852
     * @param array $methods_matched
853
     * @return void
854
     */
855
    protected function handleRouteCallback(Route $route, RouteCollection $matched, array $methods_matched)
856
    {
857
        // Handle the callback
858
        $returned = call_user_func(
859
            $route->getCallback(), // Instead of relying on the slower "invoke" magic
860
            $this->request,
861
            $this->response,
862
            $this->service,
863
            $this->app,
864
            $this, // Pass the Klein instance
865
            $matched,
866
            $methods_matched
867
        );
868
869
        if ($returned instanceof AbstractResponse) {
870
            $this->response = $returned;
871
        } else {
872
            // Otherwise, attempt to append the returned data
873
            try {
874
                $this->response->append($returned);
875
            } catch (LockedResponseException $e) {
876
                // Do nothing, since this is an automated behavior
877
            }
878
        }
879
    }
880
881
    /**
882
     * Adds an error callback to the stack of error handlers
883
     *
884
     * @param callable $callback            The callable function to execute in the error handling chain
885
     * @return boolean|void
886
     */
887
    public function onError($callback)
888
    {
889
        $this->errorCallbacks[] = $callback;
890
    }
891
892
    /**
893
     * Routes an exception through the error callbacks
894
     *
895
     * @param Exception $err        The exception that occurred
896
     * @throws UnhandledException   If the error/exception isn't handled by an error callback
897
     * @return void
898
     */
899
    protected function error(Exception $err)
900
    {
901
        $type = get_class($err);
902
        $msg = $err->getMessage();
903
904
        if (count($this->errorCallbacks) > 0) {
905
            foreach (array_reverse($this->errorCallbacks) as $callback) {
906
                if (is_callable($callback)) {
907
                    if (is_string($callback)) {
908
                        $callback($this, $msg, $type, $err);
909
910
                        return;
911
                    } else {
912
                        call_user_func($callback, $this, $msg, $type, $err);
913
914
                        return;
915
                    }
916
                } else {
917
                    if (null !== $this->service && null !== $this->response) {
918
                        $this->service->flash($err);
919
                        $this->response->redirect($callback);
920
                    }
921
                }
922
            }
923
        } else {
924
            $this->response->code(500);
925
            throw new UnhandledException($msg, $err->getCode(), $err);
926
        }
927
928
        // Lock our response, since we probably don't want
929
        // anything else messing with our error code/body
930
        $this->response->lock();
931
    }
932
933
    /**
934
     * Adds an HTTP error callback to the stack of HTTP error handlers
935
     *
936
     * @param callable $callback            The callable function to execute in the error handling chain
937
     * @return void
938
     */
939
    public function onHttpError($callback)
940
    {
941
        $this->httpErrorCallbacks[] = $callback;
942
    }
943
944
    /**
945
     * Handles an HTTP error exception through our HTTP error callbacks
946
     *
947
     * @param HttpExceptionInterface $http_exception    The exception that occurred
948
     * @param RouteCollection $matched                  The collection of routes that were matched in dispatch
949
     * @param array $methods_matched                    The HTTP methods that were matched in dispatch
950
     * @return void
951
     */
952
    protected function httpError(HttpExceptionInterface $http_exception, RouteCollection $matched, $methods_matched)
953
    {
954
        if (!$this->response->isLocked()) {
955
            $this->response->code($http_exception->getCode());
956
        }
957
958
        if (count($this->httpErrorCallbacks) > 0) {
959
            foreach (array_reverse($this->httpErrorCallbacks) as $callback) {
960
                if ($callback instanceof Route) {
961
                    $this->handleRouteCallback($callback, $matched, $methods_matched);
962
                } elseif (is_callable($callback)) {
963
                    if (is_string($callback)) {
964
                        $callback(
965
                            $http_exception->getCode(),
966
                            $this,
967
                            $matched,
968
                            $methods_matched,
969
                            $http_exception
970
                        );
971
                    } else {
972
                        call_user_func(
973
                            $callback,
974
                            $http_exception->getCode(),
975
                            $this,
976
                            $matched,
977
                            $methods_matched,
978
                            $http_exception
979
                        );
980
                    }
981
                }
982
            }
983
        }
984
985
        // Lock our response, since we probably don't want
986
        // anything else messing with our error code/body
987
        $this->response->lock();
988
    }
989
990
    /**
991
     * Adds a callback to the stack of handlers to run after the dispatch
992
     * loop has handled all of the route callbacks and before the response
993
     * is sent
994
     *
995
     * @param callable $callback            The callable function to execute in the after route chain
996
     * @return void
997
     */
998
    public function afterDispatch($callback)
999
    {
1000
        $this->afterFilterCallbacks[] = $callback;
1001
    }
1002
1003
    /**
1004
     * Runs through and executes the after dispatch callbacks
1005
     *
1006
     * @return void
1007
     */
1008
    protected function callAfterDispatchCallbacks()
1009
    {
1010
        try {
1011
            foreach ($this->afterFilterCallbacks as $callback) {
1012
                if (is_callable($callback)) {
1013
                    if (is_string($callback)) {
1014
                        $callback($this);
1015
1016
                    } else {
1017
                        call_user_func($callback, $this);
1018
1019
                    }
1020
                }
1021
            }
1022
        } catch (Exception $e) {
1023
            $this->error($e);
1024
        }
1025
    }
1026
1027
1028
    /**
1029
     * Method aliases
1030
     */
1031
1032
    /**
1033
     * Quick alias to skip the current callback/route method from executing
1034
     *
1035
     * @throws DispatchHaltedException To halt/skip the current dispatch loop
1036
     * @return void
1037
     */
1038
    public function skipThis()
1039
    {
1040
        throw new DispatchHaltedException(null, DispatchHaltedException::SKIP_THIS);
1041
    }
1042
1043
    /**
1044
     * Quick alias to skip the next callback/route method from executing
1045
     *
1046
     * @param int $num The number of next matches to skip
1047
     * @throws DispatchHaltedException To halt/skip the current dispatch loop
1048
     * @return void
1049
     */
1050
    public function skipNext($num = 1)
1051
    {
1052
        $skip = new DispatchHaltedException(null, DispatchHaltedException::SKIP_NEXT);
1053
        $skip->setNumberOfSkips($num);
1054
1055
        throw $skip;
1056
    }
1057
1058
    /**
1059
     * Quick alias to stop the remaining callbacks/route methods from executing
1060
     *
1061
     * @throws DispatchHaltedException To halt/skip the current dispatch loop
1062
     * @return void
1063
     */
1064
    public function skipRemaining()
1065
    {
1066
        throw new DispatchHaltedException(null, DispatchHaltedException::SKIP_REMAINING);
1067
    }
1068
1069
    /**
1070
     * Alias to set a response code, lock the response, and halt the route matching/dispatching
1071
     *
1072
     * @param int $code     Optional HTTP status code to send
1073
     * @throws DispatchHaltedException To halt/skip the current dispatch loop
1074
     * @return void
1075
     */
1076
    public function abort($code = null)
1077
    {
1078
        if (null !== $code) {
1079
            throw HttpException::createFromCode($code);
1080
        }
1081
1082
        throw new DispatchHaltedException();
1083
    }
1084
1085
    /**
1086
     * OPTIONS alias for "respond()"
1087
     *
1088
     * @see Klein::respond()
1089
     * @param string $path
1090
     * @param callable $callback
1091
     * @return Route
1092
     */
1093 View Code Duplication
    public function options($path = '*', $callback = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1094
    {
1095
        // Options the arguments in a very loose format
1096
        extract(
1097
            $this->parseLooseArgumentOrder(func_get_args()),
0 ignored issues
show
Bug introduced by
$this->parseLooseArgumentOrder(func_get_args()) cannot be passed to extract() as the parameter $var_array expects a reference.
Loading history...
1098
            EXTR_OVERWRITE
1099
        );
1100
1101
        return $this->respond('OPTIONS', $path, $callback);
1102
    }
1103
1104
    /**
1105
     * HEAD alias for "respond()"
1106
     *
1107
     * @see Klein::respond()
1108
     * @param string $path
1109
     * @param callable $callback
1110
     * @return Route
1111
     */
1112 View Code Duplication
    public function head($path = '*', $callback = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1113
    {
1114
        // Get the arguments in a very loose format
1115
        extract(
1116
            $this->parseLooseArgumentOrder(func_get_args()),
0 ignored issues
show
Bug introduced by
$this->parseLooseArgumentOrder(func_get_args()) cannot be passed to extract() as the parameter $var_array expects a reference.
Loading history...
1117
            EXTR_OVERWRITE
1118
        );
1119
1120
        return $this->respond('HEAD', $path, $callback);
1121
    }
1122
1123
    /**
1124
     * GET alias for "respond()"
1125
     *
1126
     * @see Klein::respond()
1127
     * @param string $path
1128
     * @param callable $callback
1129
     * @return Route
1130
     */
1131 View Code Duplication
    public function get($path = '*', $callback = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1132
    {
1133
        // Get the arguments in a very loose format
1134
        extract(
1135
            $this->parseLooseArgumentOrder(func_get_args()),
0 ignored issues
show
Bug introduced by
$this->parseLooseArgumentOrder(func_get_args()) cannot be passed to extract() as the parameter $var_array expects a reference.
Loading history...
1136
            EXTR_OVERWRITE
1137
        );
1138
1139
        return $this->respond('GET', $path, $callback);
1140
    }
1141
1142
    /**
1143
     * POST alias for "respond()"
1144
     *
1145
     * @see Klein::respond()
1146
     * @param string $path
1147
     * @param callable $callback
1148
     * @return Route
1149
     */
1150 View Code Duplication
    public function post($path = '*', $callback = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1151
    {
1152
        // Get the arguments in a very loose format
1153
        extract(
1154
            $this->parseLooseArgumentOrder(func_get_args()),
0 ignored issues
show
Bug introduced by
$this->parseLooseArgumentOrder(func_get_args()) cannot be passed to extract() as the parameter $var_array expects a reference.
Loading history...
1155
            EXTR_OVERWRITE
1156
        );
1157
1158
        return $this->respond('POST', $path, $callback);
1159
    }
1160
1161
    /**
1162
     * PUT alias for "respond()"
1163
     *
1164
     * @see Klein::respond()
1165
     * @param string $path
1166
     * @param callable $callback
1167
     * @return Route
1168
     */
1169 View Code Duplication
    public function put($path = '*', $callback = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1170
    {
1171
        // Get the arguments in a very loose format
1172
        extract(
1173
            $this->parseLooseArgumentOrder(func_get_args()),
0 ignored issues
show
Bug introduced by
$this->parseLooseArgumentOrder(func_get_args()) cannot be passed to extract() as the parameter $var_array expects a reference.
Loading history...
1174
            EXTR_OVERWRITE
1175
        );
1176
1177
        return $this->respond('PUT', $path, $callback);
1178
    }
1179
1180
    /**
1181
     * DELETE alias for "respond()"
1182
     *
1183
     * @see Klein::respond()
1184
     * @param string $path
1185
     * @param callable $callback
1186
     * @return Route
1187
     */
1188 View Code Duplication
    public function delete($path = '*', $callback = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1189
    {
1190
        // Get the arguments in a very loose format
1191
        extract(
1192
            $this->parseLooseArgumentOrder(func_get_args()),
0 ignored issues
show
Bug introduced by
$this->parseLooseArgumentOrder(func_get_args()) cannot be passed to extract() as the parameter $var_array expects a reference.
Loading history...
1193
            EXTR_OVERWRITE
1194
        );
1195
1196
        return $this->respond('DELETE', $path, $callback);
1197
    }
1198
1199
    /**
1200
     * PATCH alias for "respond()"
1201
     *
1202
     * PATCH was added to HTTP/1.1 in RFC5789
1203
     *
1204
     * @link http://tools.ietf.org/html/rfc5789
1205
     * @see Klein::respond()
1206
     * @param string $path
1207
     * @param callable $callback
1208
     * @return Route
1209
     */
1210 View Code Duplication
    public function patch($path = '*', $callback = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1211
    {
1212
        // Get the arguments in a very loose format
1213
        extract(
1214
            $this->parseLooseArgumentOrder(func_get_args()),
0 ignored issues
show
Bug introduced by
$this->parseLooseArgumentOrder(func_get_args()) cannot be passed to extract() as the parameter $var_array expects a reference.
Loading history...
1215
            EXTR_OVERWRITE
1216
        );
1217
1218
        return $this->respond('PATCH', $path, $callback);
1219
    }
1220
}
1221