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) { |
|
|
|
|
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
|
|
|
|
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.