Completed
Push — master ( 8b9a97...e8ce77 )
by Riikka
02:21
created

Router::getMatchingRoutes()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 12
cts 12
cp 1
rs 8.9197
c 0
b 0
f 0
cc 4
eloc 12
nc 4
nop 3
crap 4
1
<?php
2
3
namespace Simply\Router;
4
5
use Simply\Router\Exception\MethodNotAllowedException;
6
use Simply\Router\Exception\RouteNotFoundException;
7
8
/**
9
 * Class for routing requested methods and paths to specific routes.
10
 * @author Riikka Kalliomäki <[email protected]>
11
 * @copyright Copyright (c) 2018 Riikka Kalliomäki
12
 * @license http://opensource.org/licenses/mit-license.php MIT License
13
 */
14
class Router
15
{
16
    /** @var RouteDefinitionProvider The route definition provider */
17
    private $provider;
18
19
    /** @var string[] List of methods that would be allowed for the routed path */
20
    private $allowedMethods;
21
22
    /**
23
     * Router constructor.
24
     * @param RouteDefinitionProvider $provider The route definition provider
25
     */
26 18
    public function __construct(RouteDefinitionProvider $provider)
27
    {
28 18
        $this->provider = $provider;
29 18
        $this->allowedMethods = [];
30 18
    }
31
32
    /**
33
     * Routes the given path with the given HTTP request method to a specific route.
34
     * @param string $method The HTTP request method
35
     * @param string $path The decoded request path
36
     * @return Route Matching route
37
     * @throws MethodNotAllowedException If the path matches at least one route, but the method is not allowed
38
     * @throws RouteNotFoundException If no route matches the given path
39
     */
40 18
    public function route(string $method, string $path): Route
41
    {
42 18
        if (!HttpMethod::isValidMethod($method)) {
43 1
            throw new \InvalidArgumentException("Invalid HTTP method '$method'");
44
        }
45
46 17
        $this->allowedMethods = [];
47
48 17
        $segments = preg_split('#/#', $path, -1, PREG_SPLIT_NO_EMPTY);
49 17
        $routes = $this->matchRoutes($method, $segments);
50
51 17
        if (\count($routes) === 1) {
52 12
            return reset($routes);
53
        }
54
55 5
        if (\count($routes) > 1) {
56 1
            throw new \UnexpectedValueException("The given path '$path' matches more than one route");
57
        }
58
59 4
        if (\count($this->allowedMethods) > 0) {
60 1
            if (\in_array(HttpMethod::GET, $this->allowedMethods, true)) {
61 1
                $this->allowedMethods[] = HttpMethod::HEAD;
62
            }
63
64 1
            throw new MethodNotAllowedException(
65 1
                "The requested method '$method' is not within list of allowed methods",
66 1
                array_unique($this->allowedMethods)
67
            );
68
        }
69
70 3
        throw new RouteNotFoundException("The given path '$path' did not match any defined route");
71
    }
72
73
    /**
74
     * Returns routes that match the given HTTP request method and path segments.
75
     * @param string $method The HTTP request method
76
     * @param string[] $segments The requested path segments
77
     * @return Route[] List of matching routes
78
     */
79 17
    private function matchRoutes(string $method, array $segments): array
80
    {
81 17
        $staticRoutes = $this->provider->getStaticRoutes();
82 17
        $path = implode('/', $segments);
83
84 17
        if (isset($staticRoutes[$path])) {
85 8
            $routes = $this->getMatchingRoutes($staticRoutes[$path], $method, $segments);
86
87 8
            if ($routes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $routes of type Simply\Router\Route[] 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...
88 7
                return $routes;
89
            }
90
        }
91
92 11
        $matchedIds = $this->getIntersectingIds($segments);
93 11
        return $this->getMatchingRoutes($matchedIds, $method, $segments);
94
    }
95
96
    /**
97
     * Returns a list of route ids for routes that have matching static path segments.
98
     * @param string[] $segments The requested path segments
99
     * @return int[] List of route ids for routes that have matching static path segments
100
     */
101 11
    private function getIntersectingIds(array $segments): array
102
    {
103 11
        $count = \count($segments);
104 11
        $matched = [];
105
106 11
        $segmentCounts = $this->provider->getSegmentCounts();
107 11
        $segmentValues = $this->provider->getSegmentValues();
108
109 11
        if (empty($segmentCounts[$count])) {
110 3
            return [];
111
        }
112
113 8
        for ($i = 0; $i < $count; $i++) {
114 8
            $matched[] = ($segmentValues[$i][$segments[$i]] ?? []) + ($segmentValues[$i]['#'] ?? []);
115
        }
116
117 8
        $matched[] = $segmentCounts[$count];
118
119 8
        return array_keys($count > 0 ? array_intersect_key(... $matched) : $matched[0]);
120
    }
121
122
    /**
123
     * Returns the routes for the given ids that match the requested method and segments.
124
     * @param int[] $ids List of route ids to match
125
     * @param string $method The HTTP request method
126
     * @param string[] $segments The requested path segments
127
     * @return Route[] List of matching routes
128
     */
129 17
    private function getMatchingRoutes(array $ids, string $method, array $segments): array
130
    {
131 17
        $routes = [];
132
133 17
        foreach ($ids as $id) {
134 15
            $definition = $this->provider->getRouteDefinition($id);
135 15
            $values = [];
136
137 15
            if (!$definition->matchPatterns($segments, $values)) {
138 2
                continue;
139
            }
140
141 14
            if (!$definition->isMethodAllowed($method)) {
142 3
                array_push($this->allowedMethods, ... $definition->getMethods());
143 3
                continue;
144
            }
145
146 13
            $routes[] = new Route($definition, $method, $segments, $values);
147
        }
148
149 17
        return $routes;
150
    }
151
152
    /**
153
     * Returns the encoded URL for the route with the given name.
154
     * @param string $name Name of the route
155
     * @param string[] $parameters Values for the route parameters
156
     * @return string The encoded URL for the route
157
     */
158 9
    public function generateUrl(string $name, array $parameters = []): string
159
    {
160 9
        $definition = $this->provider->getRouteDefinitionByName($name);
161 9
        return $definition->formatUrl($parameters);
162
    }
163
}
164