Completed
Push — master ( f643b9...72cde6 )
by
unknown
02:12
created

Router::always()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 10
cts 10
cp 1
rs 9.7666
c 0
b 0
f 0
cc 2
nc 2
nop 4
crap 2
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 or presence of catch-all pattern
83
     *
84
     * @param string $patternA some pattern
85
     * @param string $patternB some pattern
86
     *
87
     * @return bool true if $patternA is before $patternB
88
     */
89 25
    public static function comparePatternSimilarity($patternA, $patternB)
90
    {
91 25
        return 0 === stripos($patternA, $patternB)
92 25
            || $patternA === AbstractRoute::CATCHALL_IDENTIFIER;
93
    }
94
95
    /**
96
     * Compares two patterns and returns the first one according to
97
     * similarity, patterns or ocurrences of a subpattern
98
     *
99
     * @param string $patternA some pattern
100
     * @param string $patternB some pattern
101
     * @param string $sub      pattern needle
102
     *
103
     * @return bool true if $patternA is before $patternB
104
     */
105 25
    public static function compareRoutePatterns($patternA, $patternB, $sub)
106
    {
107 25
        return static::comparePatternSimilarity($patternA, $patternB)
108 25
            || static::compareOcurrences($patternA, $patternB, $sub);
109
    }
110
111
    /**
112
     * Cleans up an return an array of extracted parameters
113
     *
114
     * @param array $params an array of params
115
     *
116
     * @see Respect\Rest\Request::$params
117
     *
118
     * @return array only the non-empty params
119
     */
120 117
    protected static function cleanUpParams(array $params)
121
    {
122
        //using array_values to reset array keys
123 117
        return array_values(
124 117
            array_filter(
125 117
                $params,
126
                function ($param) {
127
128
                    //remove any empty string param
129 69
                    return $param !== '';
130 117
                }
131
            )
132
        );
133
    }
134
135
    /**
136
     * Builds and appends many kinds of routes magically.
137
     *
138
     * @param string $method The HTTP method for the new route
139
     */
140 113
    public function __call($method, $args)
141
    {
142 113
        if (count($args) < 2) {
143 5
            throw new InvalidArgumentException(
144 5
                'Any route binding must at least 2 arguments'
145
            );
146
        }
147
148 108
        list($path, $routeTarget) = $args;
149
150
         // Support multiple route definitions as array of paths
151 108
        if (is_array($path)) {
152
            $lastPath = array_pop($path);
153
            foreach ($path as $p) {
154
                $this->$method($p, $routeTarget);
155
            }
156
157
            return $this->$method($lastPath, $routeTarget);
158
        }
159
160
        //closures, func names, callbacks
161 108
        if (is_callable($routeTarget)) {
162
            //raw callback
163 69
            if (!isset($args[2])) {
164 67
                return $this->callbackRoute($method, $path, $routeTarget);
165
            } else {
166 2
                return $this->callbackRoute(
167 2
                    $method,
168 2
                    $path,
169 2
                    $routeTarget,
170 2
                    $args[2]
171
                );
172
            }
173
174
        //direct instances
175 40
        } elseif ($routeTarget instanceof Routable) {
176 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...
177
178
        //static returns the argument itself
179 37
        } elseif (!is_string($routeTarget)) {
180 3
            return $this->staticRoute($method, $path, $routeTarget);
181
182
        //static returns the argument itself
183
        } elseif (
184 34
            is_string($routeTarget)
185 34
            && !(class_exists($routeTarget) || interface_exists($routeTarget))
186
        ) {
187 20
            return $this->staticRoute($method, $path, $routeTarget);
188
189
        //classes
190
        } else {
191
            //raw classnames
192 14
            if (!isset($args[2])) {
193 7
                return $this->classRoute($method, $path, $routeTarget);
194
195
             //classnames as factories
196 7
            } elseif (is_callable($args[2])) {
197 3
                return $this->factoryRoute(
198 3
                    $method,
199 3
                    $path,
200 3
                    $routeTarget,
201 3
                    $args[2]
202
                );
203
204
            //classnames with constructor arguments
205
            } else {
206 4
                return $this->classRoute(
207 4
                    $method,
208 4
                    $path,
209 4
                    $routeTarget,
210 4
                    $args[2]
211
                );
212
            }
213
        }
214
    }
215
216
    /**
217
     * @param mixed $virtualHost null for no virtual host or a string prefix
218
     *                           for every URI
219
     */
220 148
    public function __construct($virtualHost = null)
221
    {
222 148
        $this->virtualHost = $virtualHost;
223 148
    }
224
225
    /** If $this->autoDispatched, dispatches the app */
226 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...
227
    {
228 50
        if (!$this->isAutoDispatched || !isset($_SERVER['SERVER_PROTOCOL'])) {
229 39
            return;
230
        }
231
232 11
        echo $this->run();
233 11
    }
234
235
    /** Runs the router and returns its output */
236
    public function __toString()
237
    {
238
        $string = '';
239
        try {
240
            $string = (string) $this->run();
241
        } catch (\Exception $exception) {
242
            trigger_error($exception->getMessage(), E_USER_ERROR);
243
        }
244
245
        return $string;
246
    }
247
248
    /**
249
     * Applies a routine to every route
250
     *
251
     * @param string $routineName a name of some routine (Accept, When, etc)
252
     * @param array  $param1      some param
253
     * @param array  $param2      some param
254
     * @param array  $etc         This function accepts infinite params
255
     *                            that will be passed to the routine instance
256
     *
257
     * @see Respect\Rest\Request::$params
258
     *
259
     * @return Router the router itself.
260
     */
261 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...
262
    {
263 3
        $params                 = func_get_args();
264 3
        $routineName            = array_shift($params);
265 3
        $routineClassName       = 'Respect\\Rest\\Routines\\'.$routineName;
266 3
        $routineClass           = new ReflectionClass($routineClassName);
267 3
        $routineInstance        = $routineClass->newInstanceArgs($params);
268 3
        $this->globalRoutines[] = $routineInstance;
269
270 3
        foreach ($this->routes as $route) {
271 1
            $route->appendRoutine($routineInstance);
272
        }
273
274 3
        return $this;
275
    }
276
277
    /**
278
     * Appends a pre-built route to the dispatcher
279
     *
280
     * @param AbstractRoute $route Any route
281
     *
282
     * @return Router the router itself
283
     */
284 141
    public function appendRoute(AbstractRoute $route)
285
    {
286 141
        $this->routes[]     = $route;
287 141
        $route->sideRoutes  = &$this->sideRoutes;
288 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...
289
290 141
        foreach ($this->globalRoutines as $routine) {
291 1
            $route->appendRoutine($routine);
292
        }
293
294 141
        $this->sortRoutesByComplexity();
295
296 141
        return $this;
297
    }
298
299
    /**
300
     * Appends a pre-built side route to the dispatcher
301
     *
302
     * @param AbstractRoute $route Any route
303
     *
304
     * @return Router the router itself
305
     */
306
    public function appendSideRoute(AbstractRoute $route)
307
    {
308
        $this->sideRoutes[] = $route;
309
310
        foreach ($this->globalRoutines as $routine) {
311
            $route->appendRoutine($routine);
312
        }
313
314
        return $this;
315
    }
316
317
    /**
318
     * Creates and returns a callback-based route
319
     *
320
     * @param string   $method    The HTTP method
321
     * @param string   $path      The URI pattern for this route
322
     * @param callable $callback  Any callback for this route
323
     * @param array    $arguments Additional arguments for the callback
324
     *
325
     * @return Respect\Rest\Routes\Callback The route instance
326
     */
327 96
    public function callbackRoute(
328
        $method,
329
        $path,
330
        $callback,
331
        array $arguments = array()
332
    ) {
333 96
        $route = new Routes\Callback($method, $path, $callback, $arguments);
334 96
        $this->appendRoute($route);
335
336 96
        return $route;
337
    }
338
339
    /**
340
     * Creates and returns a class-based route
341
     *
342
     * @param string $method    The HTTP method
343
     * @param string $path      The URI pattern for this route
344
     * @param string $class     Some class name
345
     * @param array  $arguments The class constructor arguments
346
     *
347
     * @return Respect\Rest\Routes\ClassName The route instance
348
     */
349 12
    public function classRoute($method, $path, $class, array $arguments = array())
350
    {
351 12
        $route = new Routes\ClassName($method, $path, $class, $arguments);
352 12
        $this->appendRoute($route);
353
354 12
        return $route;
355
    }
356
357
    /**
358
     * Dispatches the router
359
     *
360
     * @param mixed $method null to infer it or an HTTP method (GET, POST, etc)
361
     * @param mixed $uri    null to infer it or a request URI path (/foo/bar)
362
     *
363
     * @return mixed Whatever you returned from your model
364
     */
365 98
    public function dispatch($method = null, $uri = null)
366
    {
367 98
        return $this->dispatchRequest(new Request($method, $uri));
368
    }
369
370
    /**
371
     * Dispatch the current route with a custom Request
372
     *
373
     * @param Request $request Some request
374
     *
375
     * @return mixed Whatever the dispatched route returns
376
     */
377 134
    public function dispatchRequest(Request $request = null)
378
    {
379 134
        if ($this->isRoutelessDispatch($request)) {
380 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...
381
        }
382
383 132
        return $this->routeDispatch();
384
    }
385
386
    /**
387
     * Creates and returns a side-route for catching exceptions
388
     *
389
     * @param string $className The name of the exception class you want to
390
     *                          catch. 'Exception' will catch them all.
391
     * @param string $callback  The function to run when an exception is cautght
392
     *
393
     * @return Respect\Rest\Routes\Exception
394
     */
395 1
    public function exceptionRoute($className, $callback = null)
396
    {
397 1
        $route = new Routes\Exception($className, $callback);
398 1
        $this->appendSideRoute($route);
399
400 1
        return $route;
401
    }
402
403
    /**
404
     * Creates and returns a side-route for catching errors
405
     *
406
     * @param string $callback The function to run when an error is cautght
407
     *
408
     * @return Respect\Rest\Routes\Error
409
     */
410 1
    public function errorRoute($callback)
411
    {
412 1
        $route = new Routes\Error($callback);
413 1
        $this->appendSideRoute($route);
414
415 1
        return $route;
416
    }
417
418
    /**
419
     * Creates and returns an factory-based route
420
     *
421
     * @param string $method    The HTTP metod (GET, POST, etc)
422
     * @param string $path      The URI Path (/foo/bar...)
423
     * @param string $className The class name of the factored instance
424
     * @param string $factory   Any callable
425
     *
426
     * @return Respect\Rest\Routes\Factory The route created
427
     */
428 3
    public function factoryRoute($method, $path, $className, $factory)
429
    {
430 3
        $route = new Routes\Factory($method, $path, $className, $factory);
431 3
        $this->appendRoute($route);
432
433 3
        return $route;
434
    }
435
436
    /**
437
     * Iterates over a list of routes and return the allowed methods for them
438
     *
439
     * @param array $routes an array of AbstractRoute
440
     *
441
     * @return array an array of unique allowed methods
442
     */
443 134
    public function getAllowedMethods(array $routes)
444
    {
445 134
        $allowedMethods = array();
446
447 134
        foreach ($routes as $route) {
448 127
            $allowedMethods[] = $route->method;
449
        }
450
451 134
        return array_unique($allowedMethods);
452
    }
453
454
    /**
455
     * Checks if router overrides the method with _method hack
456
     *
457
     * @return bool true if the router overrides current request method, false
458
     *              otherwise
459
     */
460 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...
461
    {
462 134
        return $this->request                    //Has dispatched
463 134
            && $this->methodOverriding           //Has method overriting
464 134
            && isset($_REQUEST['_method'])       //Has a hacky parameter
465 134
            && $this->request->method == 'POST'; //Only post is allowed for this
466
    }
467
468
    /**
469
     * Creates and returns an instance-based route
470
     *
471
     * @param string $method  The HTTP metod (GET, POST, etc)
472
     * @param string $path    The URI Path (/foo/bar...)
473
     * @param string $instance An instance of Routinable
474
     *
475
     * @return Respect\Rest\Routes\Instance The route created
476
     */
477 8
    public function instanceRoute($method, $path, $instance)
478
    {
479 8
        $route = new Routes\Instance($method, $path, $instance);
480 8
        $this->appendRoute($route);
481
482 8
        return $route;
483
    }
484
485
    /**
486
     * Checks if request is a global OPTIONS (OPTIONS * HTTP/1.1)
487
     *
488
     * @return bool true if the request is a global options, false otherwise
489
     */
490 134
    public function isDispatchedToGlobalOptionsMethod()
491
    {
492 134
        return $this->request->method === 'OPTIONS'
493 134
            && $this->request->uri === '*';
494
    }
495
496
    /**
497
     * Checks if a request doesn't apply for routes at all
498
     *
499
     * @param Request $request A request
500
     *
501
     * @return bool true if the request doesn't apply for routes
502
     */
503 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...
504
    {
505 134
        $this->isAutoDispatched = false;
506
507 134
        if (!$request) {
508 13
            $request = new Request();
509
        }
510
511 134
        $this->request = $request;
512
513 134
        if ($this->hasDispatchedOverridenMethod()) {
514 2
            $request->method = strtoupper($_REQUEST['_method']);
515
        }
516
517 134
        if ($this->isDispatchedToGlobalOptionsMethod()) {
518 2
            $allowedMethods = $this->getAllowedMethods($this->routes);
519
520 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...
521 2
                header('Allow: '.implode(', ', array_unique($allowedMethods)));
522
            }
523
524 2
            return true;
525
        }
526 132
    }
527
528
    /**
529
     * Performs the main route dispatching mechanism
530
     */
531 132
    public function routeDispatch()
532
    {
533 132
        $this->applyVirtualHost();
534
535 132
        $matchedByPath  = $this->getMatchedRoutesByPath();
536 132
        $allowedMethods = $this->getAllowedMethods(
537 132
            iterator_to_array($matchedByPath)
538
        );
539
540
        //OPTIONS? Let's inform the allowd methods
541 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...
542 3
            $this->handleOptionsRequest($allowedMethods, $matchedByPath);
543 129
        } elseif (0 === count($matchedByPath)) {
544 7
            header('HTTP/1.1 404');
545 122
        } elseif (!$this->routineMatch($matchedByPath) instanceof Request) {
546 6
            $this->informMethodNotAllowed($allowedMethods);
547
        }
548
549 132
        return $this->request;
550
    }
551
552
    /**
553
     * Dispatches and get response with default request parameters
554
     *
555
     * @param Request $request Some request
556
     *
557
     * @return string the response string
558
     */
559 18
    public function run(Request $request = null)
560
    {
561 18
        $route = $this->dispatchRequest($request);
562
563
        if (
564 18
            !$route
565 18
            || (isset($request->method)
566 18
                && $request->method === 'HEAD')
567
        ) {
568
            return;
569
        }
570
571 18
        $response = $route->response();
572
573 16
        if (is_resource($response)) {
574 1
            fpassthru($response);
575
576 1
            return '';
577
        }
578
579 15
        return (string) $response;
580
    }
581
582
    /**
583
     * Creates and returns a static route
584
     *
585
     * @param string $method      The HTTP method (GET, POST, etc)
586
     * @param string $path        The URI Path (/foo/bar...)
587
     * @param string $staticValue Some static value to be printed
588
     *
589
     * @return Respect\Rest\Routes\StaticValue The route created
590
     */
591 23
    public function staticRoute($method, $path, $staticValue)
592
    {
593 23
        $route = new Routes\StaticValue($method, $path, $staticValue);
594 23
        $this->appendRoute($route);
595
596 23
        return $route;
597
    }
598
599
    /** Applies the virtualHost prefix on the current request */
600 132
    protected function applyVirtualHost()
601
    {
602 132
        if ($this->virtualHost) {
603 5
            $this->request->uri = preg_replace(
604 5
                '#^'.preg_quote($this->virtualHost).'#',
605 5
                '',
606 5
                $this->request->uri
607
            );
608
        }
609 132
    }
610
611
    /**
612
     * Configures a request for a specific route with specific parameters
613
     *
614
     * @param Request       $request Some request
615
     * @param AbstractRoute $route   Some route
616
     * @param array         $params   A list of URI params
617
     *
618
     * @see Respect\Rest\Request::$params
619
     *
620
     * @return Request a configured Request instance
621
     */
622 117
    protected function configureRequest(
623
        Request $request,
624
        AbstractRoute $route,
625
        array $params = array()
626
    ) {
627 117
        $request->route = $route;
628 117
        $request->params = $params;
629
630 117
        return $request;
631
    }
632
633
    /**
634
     * Return routes matched by path
635
     *
636
     * @return SplObjectStorage a list of routes matched by path
637
     */
638 132
    protected function getMatchedRoutesByPath()
639
    {
640 132
        $matched = new \SplObjectStorage();
641
642 132
        foreach ($this->routes as $route) {
643 129
            if ($this->matchRoute($this->request, $route, $params)) {
644 125
                $matched[$route] = $params;
645
            }
646
        }
647
648 132
        return $matched;
649
    }
650
651
    /**
652
     * Sends an Allow header with allowed methods from a list
653
     *
654
     * @param array $allowedMethods A list of allowed methods
655
     *
656
     * @return null sends an Allow header.
657
     */
658 9
    protected function informAllowedMethods(array $allowedMethods)
659
    {
660 9
        header('Allow: '.implode(', ', $allowedMethods));
661 9
    }
662
663
    /**
664
     * Informs the PHP environment of a not allowed method alongside
665
     * its allowed methods for that path
666
     *
667
     * @param array $allowedMethods A list of allowed methods
668
     *
669
     * @return null sends HTTP Status Line and Allow header.
670
     */
671 6
    protected function informMethodNotAllowed(array $allowedMethods)
672
    {
673 6
        header('HTTP/1.1 405');
674
675 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...
676
            return;
677
        }
678
679 6
        $this->informAllowedMethods($allowedMethods);
680 6
        $this->request->route = null;
681 6
    }
682
683
    /**
684
     * Handles a OPTIONS request, inform of the allowed methods and
685
     * calls custom OPTIONS handler (if any).
686
     *
687
     * @param array $allowedMethods A list of allowed methods
688
     * @param \SplObjectStorage $matchedByPath A list of matched routes by path
689
     *
690
     * @return null sends Allow header.
691
     */
692 3
    protected function handleOptionsRequest(array $allowedMethods, \SplObjectStorage $matchedByPath)
693
    {
694 3
        $this->informAllowedMethods($allowedMethods);
695
696 3
        if (in_array('OPTIONS', $allowedMethods)) {
697 1
            $this->routineMatch($matchedByPath);
698
        } else {
699 2
            $this->request->route = null;
700
        }
701 3
    }
702
703
    /**
704
     * Checks if a route matches a method
705
     *
706
     * @param AbstractRoute $route      A route instance
707
     * @param string        $methodName Name of the method to match
708
     *
709
     * @return bool true if route matches
710
     */
711 123
    protected function matchesMethod(AbstractRoute $route, $methodName)
712
    {
713 123
        return 0 !== stripos($methodName, '__')
714 122
            && ($route->method === $this->request->method
715 31
                || $route->method === 'ANY'
716 7
                || ($route->method === 'GET'
717 123
                    && $this->request->method === 'HEAD'
718
                )
719
            );
720
    }
721
722
    /**
723
     * Returns true if the passed route matches the passed request
724
     *
725
     * @param Request       $request Some request
726
     * @param AbstractRoute $route   Some route
727
     * @param array         $params  A list of URI params
728
     *
729
     * @see Respect\Rest\Request::$params
730
     *
731
     * @return bool true if the route matches the request with that params
732
     */
733 129
    protected function matchRoute(
734
        Request $request,
735
        AbstractRoute $route,
736
        &$params = array()
737
    ) {
738 129
        if ($route->match($request, $params)) {
739 125
            $request->route = $route;
740
741 125
            return true;
742
        }
743 12
    }
744
745
    /**
746
     * Checks if a route matches its routines
747
     *
748
     * @param \SplObjectStorage $matchedByPath A list of routes matched by path
749
     *
750
     * @return bool true if route matches its routines
751
     */
752 123
    protected function routineMatch(\SplObjectStorage $matchedByPath)
753
    {
754 123
        $badRequest = false;
755
756 123
        foreach ($matchedByPath as $route) {
757 123
            if ($this->matchesMethod($route, $this->request->method)) {
758 119
                $tempParams = $matchedByPath[$route];
759 119
                if ($route->matchRoutines($this->request, $tempParams)) {
760 117
                    return $this->configureRequest(
761 117
                        $this->request,
762 117
                        $route,
763 117
                        static::cleanUpParams($tempParams)
764
                    );
765
                } else {
766 2
                    $badRequest = true;
767
                }
768
            }
769
        }
770
771 6
        return $badRequest ? false : null;
772
    }
773
774
    /** Sorts current routes according to path and parameters */
775 141
    protected function sortRoutesByComplexity()
776
    {
777 141
        usort(
778 141
            $this->routes,
779 141
            function ($a, $b) {
780 25
                $a = $a->pattern;
781 25
                $b = $b->pattern;
782
783
                //Compare similarity and ocurrences of "/**"
784 25
                if (Router::compareRoutePatterns($a, $b,
785 25
                        AbstractRoute::CATCHALL_IDENTIFIER))
786 17
                    return -1;
787
788
                //Compare similarity and ocurrences of "/*"
789 9
                elseif (Router::compareRoutePatterns($a, $b,
790 9
                        AbstractRoute::PARAM_IDENTIFIER))
791 4
                    return -1;
792
793
                //Compare for "/" without wildcards
794 7
                elseif (Router::compareRoutePatterns(
795 7
                        preg_replace('#(/\*+)*$#', '', $a),
796 7
                        preg_replace('#(/\*+)*$#', '', $b), '/'))
797 2
                    return 1;
798
799 6
                return 0;
800 141
            }
801
        );
802 141
    }
803
}
804