Router   F
last analyzed

Complexity

Total Complexity 94

Size/Duplication

Total Lines 767
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 91.5%

Importance

Changes 0
Metric Value
wmc 94
lcom 1
cbo 9
dl 0
loc 767
ccs 226
cts 247
cp 0.915
rs 1.833
c 0
b 0
f 0

35 Methods

Rating   Name   Duplication   Size   Complexity  
A compareOcurrences() 0 5 1
A compareRoutePatterns() 0 5 2
A cleanUpParams() 0 14 1
C __call() 0 75 13
A __construct() 0 4 1
A __destruct() 0 8 3
A __toString() 0 11 2
A always() 0 15 2
A appendRoute() 0 14 2
A appendSideRoute() 0 10 2
A callbackRoute() 0 11 1
A classRoute() 0 7 1
A dispatch() 0 4 1
A dispatchRequest() 0 8 2
A exceptionRoute() 0 7 1
A errorRoute() 0 7 1
A factoryRoute() 0 7 1
A getAllowedMethods() 0 10 2
A hasDispatchedOverridenMethod() 0 7 4
A instanceRoute() 0 7 1
A isDispatchedToGlobalOptionsMethod() 0 5 2
A isRoutelessDispatch() 0 24 5
A routeDispatch() 0 20 5
A run() 0 22 5
A staticRoute() 0 7 1
A applyVirtualHost() 0 10 2
A configureRequest() 0 10 1
A getMatchedRoutesByPath() 0 12 3
A informAllowedMethods() 0 4 1
A informMethodNotAllowed() 0 11 2
A handleOptionsRequest() 0 10 2
A matchesMethod() 0 10 5
A matchRoute() 0 11 2
A routineMatch() 0 21 5
B sortRoutesByComplexity() 0 34 9

How to fix   Complexity   

Complex Class

Complex classes like Router often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Router, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * This file is part of the Respect\Rest package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
namespace Respect\Rest;
10
11
use Exception;
12
use ReflectionClass;
13
use InvalidArgumentException;
14
use Respect\Rest\Routes\AbstractRoute;
15
16
/**
17
 * A router that contains many instances of routes.
18
 *
19
 * @method \Respect\Rest\Routes\AbstractRoute get(\string $path, $routeTarget)
20
 * @method \Respect\Rest\Routes\AbstractRoute post(\string $path, $routeTarget)
21
 * @method \Respect\Rest\Routes\AbstractRoute put(\string $path, $routeTarget)
22
 * @method \Respect\Rest\Routes\AbstractRoute delete(\string $path, $routeTarget)
23
 * @method \Respect\Rest\Routes\AbstractRoute head(\string $path, $routeTarget)
24
 * @method \Respect\Rest\Routes\AbstractRoute options(\string $path, $routeTarget)
25
 * @method \Respect\Rest\Routes\AbstractRoute patch(\string $path, $routeTarget)
26
 * @method \Respect\Rest\Routes\AbstractRoute any(\string $path, $routeTarget)
27
 */
28
class Router
29
{
30
    /**
31
     * @var bool true if this router dispatches itself when destroyed, false
32
     * otherwise
33
     */
34
    public $isAutoDispatched = true;
35
36
    /**
37
     * @var bool true if this router accepts _method HTTP hacks for PUT and
38
     * DELETE via POST
39
     */
40
    public $methodOverriding = false;
41
42
    /**
43
     * @var array An array of routines that must be applied to every route
44
     * instance
45
     */
46
    protected $globalRoutines = array();
47
48
    /**
49
     * @var array An array of main routes for this router
50
     */
51
    protected $routes = array();
52
53
    /**
54
     * @var array An array of side routes (errors, exceptions, etc) for this
55
     * router
56
     */
57
    protected $sideRoutes = array();
58
59
    /**
60
     * @var string The prefix for every requested URI starting with a slash
61
     */
62
    protected $virtualHost = '';
63
64
    /**
65
     * Compares two patterns and returns the first one according to
66
     * similarity or ocurrences of a subpattern
67
     *
68
     * @param string $patternA some pattern
69
     * @param string $patternB some pattern
70
     * @param string $sub      pattern needle
71
     *
72
     * @return bool true if $patternA is before $patternB
73
     */
74 10
    public static function compareOcurrences($patternA, $patternB, $sub)
75
    {
76 10
        return substr_count($patternA, $sub)
77 10
            < substr_count($patternB, $sub);
78
    }
79
80
    /**
81
     * Compares two patterns and returns the first one according to
82
     * similarity, patterns or ocurrences of a subpattern
83
     *
84
     * @param string $patternA some pattern
85
     * @param string $patternB some pattern
86
     * @param string $sub      pattern needle
87
     *
88
     * @return bool true if $patternA is before $patternB
89
     */
90
    public static function compareRoutePatterns($patternA, $patternB, $sub)
91
    {
92
        return static::comparePatternSimilarity($patternA, $patternB)
0 ignored issues
show
Bug introduced by
The method comparePatternSimilarity() does not seem to exist on object<Respect\Rest\Router>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
93
            || static::compareOcurrences($patternA, $patternB, $sub);
94
    }
95
96
    /**
97
     * Cleans up an return an array of extracted parameters
98
     *
99
     * @param array $params an array of params
100
     *
101
     * @see Respect\Rest\Request::$params
102
     *
103
     * @return array only the non-empty params
104
     */
105 117
    protected static function cleanUpParams(array $params)
106
    {
107
        //using array_values to reset array keys
108 117
        return array_values(
109 117
            array_filter(
110 117
                $params,
111
                function ($param) {
112
113
                    //remove any empty string param
114 69
                    return $param !== '';
115 117
                }
116
            )
117
        );
118
    }
119
120
    /**
121
     * Builds and appends many kinds of routes magically.
122
     *
123
     * @param string $method The HTTP method for the new route
124
     */
125 113
    public function __call($method, $args)
126
    {
127 113
        if (count($args) < 2) {
128 5
            throw new InvalidArgumentException(
129 5
                'Any route binding must at least 2 arguments'
130
            );
131
        }
132
133 108
        list($path, $routeTarget) = $args;
134
135
         // Support multiple route definitions as array of paths
136 108
        if (is_array($path)) {
137
            $lastPath = array_pop($path);
138
            foreach ($path as $p) {
139
                $this->$method($p, $routeTarget);
140
            }
141
142
            return $this->$method($lastPath, $routeTarget);
143
        }
144
145
        //closures, func names, callbacks
146 108
        if (is_callable($routeTarget)) {
147
            //raw callback
148 69
            if (!isset($args[2])) {
149 67
                return $this->callbackRoute($method, $path, $routeTarget);
150
            } else {
151 2
                return $this->callbackRoute(
152 2
                    $method,
153 2
                    $path,
154 2
                    $routeTarget,
155 2
                    $args[2]
156
                );
157
            }
158
159
        //direct instances
160 40
        } elseif ($routeTarget instanceof Routable) {
161 3
            return $this->instanceRoute($method, $path, $routeTarget);
0 ignored issues
show
Documentation introduced by
$routeTarget is of type object<Respect\Rest\Routable>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
162
163
        //static returns the argument itself
164 37
        } elseif (!is_string($routeTarget)) {
165 3
            return $this->staticRoute($method, $path, $routeTarget);
166
167
        //static returns the argument itself
168
        } elseif (
169 34
            is_string($routeTarget)
170 34
            && !(class_exists($routeTarget) || interface_exists($routeTarget))
171
        ) {
172 20
            return $this->staticRoute($method, $path, $routeTarget);
173
174
        //classes
175
        } else {
176
            //raw classnames
177 14
            if (!isset($args[2])) {
178 7
                return $this->classRoute($method, $path, $routeTarget);
179
180
             //classnames as factories
181 7
            } elseif (is_callable($args[2])) {
182 3
                return $this->factoryRoute(
183 3
                    $method,
184 3
                    $path,
185 3
                    $routeTarget,
186 3
                    $args[2]
187
                );
188
189
            //classnames with constructor arguments
190
            } else {
191 4
                return $this->classRoute(
192 4
                    $method,
193 4
                    $path,
194 4
                    $routeTarget,
195 4
                    $args[2]
196
                );
197
            }
198
        }
199
    }
200
201
    /**
202
     * @param mixed $virtualHost null for no virtual host or a string prefix
203
     *                           for every URI
204
     */
205 148
    public function __construct($virtualHost = null)
206
    {
207 148
        $this->virtualHost = $virtualHost;
208 148
    }
209
210
    /** If $this->autoDispatched, dispatches the app */
211 50
    public function __destruct()
0 ignored issues
show
Coding Style introduced by
__destruct uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
212
    {
213 50
        if (!$this->isAutoDispatched || !isset($_SERVER['SERVER_PROTOCOL'])) {
214 39
            return;
215
        }
216
217 11
        echo $this->run();
218 11
    }
219
220
    /** Runs the router and returns its output */
221
    public function __toString()
222
    {
223
        $string = '';
224
        try {
225
            $string = (string) $this->run();
226
        } catch (\Exception $exception) {
227
            trigger_error($exception->getMessage(), E_USER_ERROR);
228
        }
229
230
        return $string;
231
    }
232
233
    /**
234
     * Applies a routine to every route
235
     *
236
     * @param string $routineName a name of some routine (Accept, When, etc)
237
     * @param array  $param1      some param
238
     * @param array  $param2      some param
239
     * @param array  $etc         This function accepts infinite params
240
     *                            that will be passed to the routine instance
241
     *
242
     * @see Respect\Rest\Request::$params
243
     *
244
     * @return Router the router itself.
245
     */
246 3
    public function always($routineName, $param1 = null, $param2 = null, $etc = null)
0 ignored issues
show
Unused Code introduced by
The parameter $routineName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $param1 is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $param2 is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $etc is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
247
    {
248 3
        $params                 = func_get_args();
249 3
        $routineName            = array_shift($params);
250 3
        $routineClassName       = 'Respect\\Rest\\Routines\\'.$routineName;
251 3
        $routineClass           = new ReflectionClass($routineClassName);
252 3
        $routineInstance        = $routineClass->newInstanceArgs($params);
253 3
        $this->globalRoutines[] = $routineInstance;
254
255 3
        foreach ($this->routes as $route) {
256 1
            $route->appendRoutine($routineInstance);
257
        }
258
259 3
        return $this;
260
    }
261
262
    /**
263
     * Appends a pre-built route to the dispatcher
264
     *
265
     * @param AbstractRoute $route Any route
266
     *
267
     * @return Router the router itself
268
     */
269 141
    public function appendRoute(AbstractRoute $route)
270
    {
271 141
        $this->routes[]     = $route;
272 141
        $route->sideRoutes  = &$this->sideRoutes;
273 141
        $route->virtualHost = $this->virtualHost;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->virtualHost of type string is incompatible with the declared type array of property $virtualHost.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
274
275 141
        foreach ($this->globalRoutines as $routine) {
276 1
            $route->appendRoutine($routine);
277
        }
278
279 141
        $this->sortRoutesByComplexity();
280
281 141
        return $this;
282
    }
283
284
    /**
285
     * Appends a pre-built side route to the dispatcher
286
     *
287
     * @param AbstractRoute $route Any route
288
     *
289
     * @return Router the router itself
290
     */
291
    public function appendSideRoute(AbstractRoute $route)
292
    {
293
        $this->sideRoutes[] = $route;
294
295
        foreach ($this->globalRoutines as $routine) {
296
            $route->appendRoutine($routine);
297
        }
298
299
        return $this;
300
    }
301
302
    /**
303
     * Creates and returns a callback-based route
304
     *
305
     * @param string   $method    The HTTP method
306
     * @param string   $path      The URI pattern for this route
307
     * @param callable $callback  Any callback for this route
308
     * @param array    $arguments Additional arguments for the callback
309
     *
310
     * @return Respect\Rest\Routes\Callback The route instance
311
     */
312 96
    public function callbackRoute(
313
        $method,
314
        $path,
315
        $callback,
316
        array $arguments = array()
317
    ) {
318 96
        $route = new Routes\Callback($method, $path, $callback, $arguments);
319 96
        $this->appendRoute($route);
320
321 96
        return $route;
322
    }
323
324
    /**
325
     * Creates and returns a class-based route
326
     *
327
     * @param string $method    The HTTP method
328
     * @param string $path      The URI pattern for this route
329
     * @param string $class     Some class name
330
     * @param array  $arguments The class constructor arguments
331
     *
332
     * @return Respect\Rest\Routes\ClassName The route instance
333
     */
334 12
    public function classRoute($method, $path, $class, array $arguments = array())
335
    {
336 12
        $route = new Routes\ClassName($method, $path, $class, $arguments);
337 12
        $this->appendRoute($route);
338
339 12
        return $route;
340
    }
341
342
    /**
343
     * Dispatches the router
344
     *
345
     * @param mixed $method null to infer it or an HTTP method (GET, POST, etc)
346
     * @param mixed $uri    null to infer it or a request URI path (/foo/bar)
347
     *
348
     * @return mixed Whatever you returned from your model
349
     */
350 98
    public function dispatch($method = null, $uri = null)
351
    {
352 98
        return $this->dispatchRequest(new Request($method, $uri));
353
    }
354
355
    /**
356
     * Dispatch the current route with a custom Request
357
     *
358
     * @param Request $request Some request
359
     *
360
     * @return mixed Whatever the dispatched route returns
361
     */
362 134
    public function dispatchRequest(Request $request = null)
363
    {
364 134
        if ($this->isRoutelessDispatch($request)) {
365 2
            return $this->request;
0 ignored issues
show
Bug introduced by
The property request does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
366
        }
367
368 132
        return $this->routeDispatch();
369
    }
370
371
    /**
372
     * Creates and returns a side-route for catching exceptions
373
     *
374
     * @param string $className The name of the exception class you want to
375
     *                          catch. 'Exception' will catch them all.
376
     * @param string $callback  The function to run when an exception is cautght
377
     *
378
     * @return Respect\Rest\Routes\Exception
379
     */
380 1
    public function exceptionRoute($className, $callback = null)
381
    {
382 1
        $route = new Routes\Exception($className, $callback);
383 1
        $this->appendSideRoute($route);
384
385 1
        return $route;
386
    }
387
388
    /**
389
     * Creates and returns a side-route for catching errors
390
     *
391
     * @param string $callback The function to run when an error is cautght
392
     *
393
     * @return Respect\Rest\Routes\Error
394
     */
395 1
    public function errorRoute($callback)
396
    {
397 1
        $route = new Routes\Error($callback);
398 1
        $this->appendSideRoute($route);
399
400 1
        return $route;
401
    }
402
403
    /**
404
     * Creates and returns an factory-based route
405
     *
406
     * @param string $method    The HTTP metod (GET, POST, etc)
407
     * @param string $path      The URI Path (/foo/bar...)
408
     * @param string $className The class name of the factored instance
409
     * @param string $factory   Any callable
410
     *
411
     * @return Respect\Rest\Routes\Factory The route created
412
     */
413 3
    public function factoryRoute($method, $path, $className, $factory)
414
    {
415 3
        $route = new Routes\Factory($method, $path, $className, $factory);
416 3
        $this->appendRoute($route);
417
418 3
        return $route;
419
    }
420
421
    /**
422
     * Iterates over a list of routes and return the allowed methods for them
423
     *
424
     * @param array $routes an array of AbstractRoute
425
     *
426
     * @return array an array of unique allowed methods
427
     */
428 134
    public function getAllowedMethods(array $routes)
429
    {
430 134
        $allowedMethods = array();
431
432 134
        foreach ($routes as $route) {
433 127
            $allowedMethods[] = $route->method;
434
        }
435
436 134
        return array_unique($allowedMethods);
437
    }
438
439
    /**
440
     * Checks if router overrides the method with _method hack
441
     *
442
     * @return bool true if the router overrides current request method, false
443
     *              otherwise
444
     */
445 134
    public function hasDispatchedOverridenMethod()
0 ignored issues
show
Coding Style introduced by
hasDispatchedOverridenMethod uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
446
    {
447 134
        return $this->request                    //Has dispatched
448 134
            && $this->methodOverriding           //Has method overriting
449 134
            && isset($_REQUEST['_method'])       //Has a hacky parameter
450 134
            && $this->request->method == 'POST'; //Only post is allowed for this
451
    }
452
453
    /**
454
     * Creates and returns an instance-based route
455
     *
456
     * @param string $method  The HTTP metod (GET, POST, etc)
457
     * @param string $path    The URI Path (/foo/bar...)
458
     * @param string $instance An instance of Routinable
459
     *
460
     * @return Respect\Rest\Routes\Instance The route created
461
     */
462 8
    public function instanceRoute($method, $path, $instance)
463
    {
464 8
        $route = new Routes\Instance($method, $path, $instance);
465 8
        $this->appendRoute($route);
466
467 8
        return $route;
468
    }
469
470
    /**
471
     * Checks if request is a global OPTIONS (OPTIONS * HTTP/1.1)
472
     *
473
     * @return bool true if the request is a global options, false otherwise
474
     */
475 134
    public function isDispatchedToGlobalOptionsMethod()
476
    {
477 134
        return $this->request->method === 'OPTIONS'
478 134
            && $this->request->uri === '*';
479
    }
480
481
    /**
482
     * Checks if a request doesn't apply for routes at all
483
     *
484
     * @param Request $request A request
485
     *
486
     * @return bool true if the request doesn't apply for routes
487
     */
488 134
    public function isRoutelessDispatch(Request $request = null)
0 ignored issues
show
Coding Style introduced by
isRoutelessDispatch uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
489
    {
490 134
        $this->isAutoDispatched = false;
491
492 134
        if (!$request) {
493 13
            $request = new Request();
494
        }
495
496 134
        $this->request = $request;
497
498 134
        if ($this->hasDispatchedOverridenMethod()) {
499 2
            $request->method = strtoupper($_REQUEST['_method']);
500
        }
501
502 134
        if ($this->isDispatchedToGlobalOptionsMethod()) {
503 2
            $allowedMethods = $this->getAllowedMethods($this->routes);
504
505 2
            if ($allowedMethods) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowedMethods of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
506 2
                header('Allow: '.implode(', ', array_unique($allowedMethods)));
507
            }
508
509 2
            return true;
510
        }
511 132
    }
512
513
    /**
514
     * Performs the main route dispatching mechanism
515
     */
516 132
    public function routeDispatch()
517
    {
518 132
        $this->applyVirtualHost();
519
520 132
        $matchedByPath  = $this->getMatchedRoutesByPath();
521 132
        $allowedMethods = $this->getAllowedMethods(
522 132
            iterator_to_array($matchedByPath)
523
        );
524
525
        //OPTIONS? Let's inform the allowd methods
526 132
        if ($this->request->method === 'OPTIONS' && $allowedMethods) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowedMethods of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
527 3
            $this->handleOptionsRequest($allowedMethods, $matchedByPath);
528 129
        } elseif (0 === count($matchedByPath)) {
529 7
            header('HTTP/1.1 404');
530 122
        } elseif (!$this->routineMatch($matchedByPath) instanceof Request) {
531 6
            $this->informMethodNotAllowed($allowedMethods);
532
        }
533
534 132
        return $this->request;
535
    }
536
537
    /**
538
     * Dispatches and get response with default request parameters
539
     *
540
     * @param Request $request Some request
541
     *
542
     * @return string the response string
543
     */
544 18
    public function run(Request $request = null)
545
    {
546 18
        $route = $this->dispatchRequest($request);
547
548
        if (
549 18
            !$route
550 18
            || (isset($request->method)
551 18
                && $request->method === 'HEAD')
552
        ) {
553
            return;
554
        }
555
556 18
        $response = $route->response();
557
558 16
        if (is_resource($response)) {
559 1
            fpassthru($response);
560
561 1
            return '';
562
        }
563
564 15
        return (string) $response;
565
    }
566
567
    /**
568
     * Creates and returns a static route
569
     *
570
     * @param string $method      The HTTP method (GET, POST, etc)
571
     * @param string $path        The URI Path (/foo/bar...)
572
     * @param string $staticValue Some static value to be printed
573
     *
574
     * @return Respect\Rest\Routes\StaticValue The route created
575
     */
576 23
    public function staticRoute($method, $path, $staticValue)
577
    {
578 23
        $route = new Routes\StaticValue($method, $path, $staticValue);
579 23
        $this->appendRoute($route);
580
581 23
        return $route;
582
    }
583
584
    /** Applies the virtualHost prefix on the current request */
585 132
    protected function applyVirtualHost()
586
    {
587 132
        if ($this->virtualHost) {
588 5
            $this->request->uri = preg_replace(
589 5
                '#^'.preg_quote($this->virtualHost).'#',
590 5
                '',
591 5
                $this->request->uri
592
            );
593
        }
594 132
    }
595
596
    /**
597
     * Configures a request for a specific route with specific parameters
598
     *
599
     * @param Request       $request Some request
600
     * @param AbstractRoute $route   Some route
601
     * @param array         $params   A list of URI params
602
     *
603
     * @see Respect\Rest\Request::$params
604
     *
605
     * @return Request a configured Request instance
606
     */
607 117
    protected function configureRequest(
608
        Request $request,
609
        AbstractRoute $route,
610
        array $params = array()
611
    ) {
612 117
        $request->route = $route;
613 117
        $request->params = $params;
614
615 117
        return $request;
616
    }
617
618
    /**
619
     * Return routes matched by path
620
     *
621
     * @return SplObjectStorage a list of routes matched by path
622
     */
623 132
    protected function getMatchedRoutesByPath()
624
    {
625 132
        $matched = new \SplObjectStorage();
626
627 132
        foreach ($this->routes as $route) {
628 129
            if ($this->matchRoute($this->request, $route, $params)) {
629 125
                $matched[$route] = $params;
630
            }
631
        }
632
633 132
        return $matched;
634
    }
635
636
    /**
637
     * Sends an Allow header with allowed methods from a list
638
     *
639
     * @param array $allowedMethods A list of allowed methods
640
     *
641
     * @return null sends an Allow header.
642
     */
643 9
    protected function informAllowedMethods(array $allowedMethods)
644
    {
645 9
        header('Allow: '.implode(', ', $allowedMethods));
646 9
    }
647
648
    /**
649
     * Informs the PHP environment of a not allowed method alongside
650
     * its allowed methods for that path
651
     *
652
     * @param array $allowedMethods A list of allowed methods
653
     *
654
     * @return null sends HTTP Status Line and Allow header.
655
     */
656 6
    protected function informMethodNotAllowed(array $allowedMethods)
657
    {
658 6
        header('HTTP/1.1 405');
659
660 6
        if (!$allowedMethods) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowedMethods of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
661
            return;
662
        }
663
664 6
        $this->informAllowedMethods($allowedMethods);
665 6
        $this->request->route = null;
666 6
    }
667
668
    /**
669
     * Handles a OPTIONS request, inform of the allowed methods and
670
     * calls custom OPTIONS handler (if any).
671
     *
672
     * @param array $allowedMethods A list of allowed methods
673
     * @param \SplObjectStorage $matchedByPath A list of matched routes by path
674
     *
675
     * @return null sends Allow header.
676
     */
677 3
    protected function handleOptionsRequest(array $allowedMethods, \SplObjectStorage $matchedByPath)
678
    {
679 3
        $this->informAllowedMethods($allowedMethods);
680
681 3
        if (in_array('OPTIONS', $allowedMethods)) {
682 1
            $this->routineMatch($matchedByPath);
683
        } else {
684 2
            $this->request->route = null;
685
        }
686 3
    }
687
688
    /**
689
     * Checks if a route matches a method
690
     *
691
     * @param AbstractRoute $route      A route instance
692
     * @param string        $methodName Name of the method to match
693
     *
694
     * @return bool true if route matches
695
     */
696 123
    protected function matchesMethod(AbstractRoute $route, $methodName)
697
    {
698 123
        return 0 !== stripos($methodName, '__')
699 122
            && ($route->method === $this->request->method
700 31
                || $route->method === 'ANY'
701 7
                || ($route->method === 'GET'
702 123
                    && $this->request->method === 'HEAD'
703
                )
704
            );
705
    }
706
707
    /**
708
     * Returns true if the passed route matches the passed request
709
     *
710
     * @param Request       $request Some request
711
     * @param AbstractRoute $route   Some route
712
     * @param array         $params  A list of URI params
713
     *
714
     * @see Respect\Rest\Request::$params
715
     *
716
     * @return bool true if the route matches the request with that params
717
     */
718 129
    protected function matchRoute(
719
        Request $request,
720
        AbstractRoute $route,
721
        &$params = array()
722
    ) {
723 129
        if ($route->match($request, $params)) {
724 125
            $request->route = $route;
725
726 125
            return true;
727
        }
728 12
    }
729
730
    /**
731
     * Checks if a route matches its routines
732
     *
733
     * @param \SplObjectStorage $matchedByPath A list of routes matched by path
734
     *
735
     * @return bool true if route matches its routines
736
     */
737 123
    protected function routineMatch(\SplObjectStorage $matchedByPath)
738
    {
739 123
        $badRequest = false;
740
741 123
        foreach ($matchedByPath as $route) {
742 123
            if ($this->matchesMethod($route, $this->request->method)) {
743 119
                $tempParams = $matchedByPath[$route];
744 119
                if ($route->matchRoutines($this->request, $tempParams)) {
745 117
                    return $this->configureRequest(
746 117
                        $this->request,
747 117
                        $route,
748 117
                        static::cleanUpParams($tempParams)
749
                    );
750
                } else {
751 2
                    $badRequest = true;
752
                }
753
            }
754
        }
755
756 6
        return $badRequest ? false : null;
757
    }
758
759
    /** Sorts current routes according to path and parameters */
760 141
    protected function sortRoutesByComplexity()
761
    {
762 141
        usort(
763 141
            $this->routes,
764 141
            function ($a, $b) {
765 25
                $a = $a->pattern;
766 25
                $b = $b->pattern;
767
768
                //Compare the same
769 25
                if ($a === $b)
770 15
                    return 0;
771
772
                //Compare occurences of '/' reusable
773 10
                $slash_count = Router::compareOcurrences($a, $b, '/');
774
775
                //Compare catch all "/**"
776 10
                $a_catchall = preg_match('#/\*\*$#', $a);
777 10
                $b_catchall = preg_match('#/\*\*$#', $b);
778 10
                if ($a_catchall != $b_catchall)
779 1
                    return $a_catchall ? 1 : -1;
780
                //Compare occurances of '/' of two catch alls
781 9
                if ($a_catchall && $b_catchall)
782
                    return $slash_count ? 1 : -1;
783
784
                //Compare ocurrences of "/*"
785 9
                if (Router::compareOcurrences($a, $b,
786 9
                        AbstractRoute::PARAM_IDENTIFIER))
787 4
                    return -1;
788
789
                //Compare occurances of '/'
790 7
                return $slash_count ? -1 : 1;
791 141
            }
792
        );
793 141
    }
794
}
795