@@ -23,272 +23,272 @@ |
||
| 23 | 23 | class Matcher |
| 24 | 24 | { |
| 25 | 25 | |
| 26 | - /** |
|
| 27 | - * @var Collector |
|
| 28 | - */ |
|
| 29 | - |
|
| 30 | - protected $collector; |
|
| 31 | - |
|
| 32 | - /** |
|
| 33 | - * Define a basepath to all routes. |
|
| 34 | - * |
|
| 35 | - * @var string |
|
| 36 | - */ |
|
| 37 | - |
|
| 38 | - protected $basepath = ""; |
|
| 39 | - |
|
| 40 | - /** |
|
| 41 | - * Construct the route dispatcher. |
|
| 42 | - * |
|
| 43 | - * @param Collector $collector |
|
| 44 | - * @param string $basepath Define a Path prefix that must be excluded on matches. |
|
| 45 | - */ |
|
| 46 | - |
|
| 47 | - public function __construct(Collector $collector, $basepath = "") |
|
| 48 | - { |
|
| 49 | - $this->collector = $collector; |
|
| 50 | - $this->basepath = $basepath; |
|
| 51 | - } |
|
| 52 | - |
|
| 53 | - /** |
|
| 54 | - * Find a route that matches the given arguments. |
|
| 55 | - * |
|
| 56 | - * @param string $httpMethod |
|
| 57 | - * @param string $path |
|
| 58 | - * |
|
| 59 | - * @throws NotFoundException |
|
| 60 | - * @throws MethodNotAllowedException |
|
| 61 | - * |
|
| 62 | - * @return Route |
|
| 63 | - */ |
|
| 64 | - |
|
| 65 | - public function match($httpMethod, $path) |
|
| 66 | - { |
|
| 67 | - $path = $this->parsePath($path); |
|
| 68 | - |
|
| 69 | - if ($route = $this->collector->findStaticRoute($httpMethod, $path)) { |
|
| 70 | - return $route; |
|
| 71 | - } |
|
| 72 | - |
|
| 73 | - if ($route = $this->matchDynamicRoute($httpMethod, $path)) { |
|
| 74 | - return $route; |
|
| 75 | - } |
|
| 76 | - |
|
| 77 | - $this->matchSimilarRoute($httpMethod, $path); |
|
| 78 | - } |
|
| 79 | - |
|
| 80 | - /** |
|
| 81 | - * Find and return the request dynamic route based on the compiled data and Path. |
|
| 82 | - * |
|
| 83 | - * @param string $httpMethod |
|
| 84 | - * @param string $path |
|
| 85 | - * |
|
| 86 | - * @return Route|false If the request match an array with the action and parameters will |
|
| 87 | - * be returned otherwise a false will. |
|
| 88 | - */ |
|
| 89 | - |
|
| 90 | - protected function matchDynamicRoute($httpMethod, $path) |
|
| 91 | - { |
|
| 92 | - if ($routes = $this->collector->findDynamicRoutes($httpMethod, $path)) { |
|
| 93 | - // chunk routes for smaller regex groups using the Sturges' Formula |
|
| 94 | - foreach (array_chunk($routes, round(1 + 3.3 * log(count($routes))), true) as $chunk) { |
|
| 95 | - array_map([$this, "buildRoute"], $chunk); |
|
| 96 | - list($pattern, $map) = $this->buildGroup($chunk); |
|
| 97 | - |
|
| 98 | - if (!preg_match($pattern, $path, $matches)) { |
|
| 99 | - continue; |
|
| 100 | - } |
|
| 101 | - |
|
| 102 | - /** @var Route $route */ |
|
| 103 | - $route = $map[count($matches)]; |
|
| 104 | - unset($matches[0]); |
|
| 105 | - |
|
| 106 | - $route->setParams(array_combine($route->getParams(), array_filter($matches))); |
|
| 107 | - $route->setMatcher($this); |
|
| 108 | - |
|
| 109 | - return $route; |
|
| 110 | - } |
|
| 111 | - } |
|
| 112 | - |
|
| 113 | - return false; |
|
| 114 | - } |
|
| 115 | - |
|
| 116 | - /** |
|
| 117 | - * Parse the dynamic segments of the pattern and replace then for |
|
| 118 | - * corresponding regex. |
|
| 119 | - * |
|
| 120 | - * @param Route $route |
|
| 121 | - * @return Route |
|
| 122 | - */ |
|
| 123 | - |
|
| 124 | - protected function buildRoute(Route $route) |
|
| 125 | - { |
|
| 126 | - if ($route->getBlock()) { |
|
| 127 | - return $route; |
|
| 128 | - } |
|
| 129 | - |
|
| 130 | - list($pattern, $params) = $this->parsePlaceholders($route->getPattern()); |
|
| 131 | - return $route->setPatternWithoutReset($pattern)->setParams($params)->setBlock(true); |
|
| 132 | - } |
|
| 133 | - |
|
| 134 | - /** |
|
| 135 | - * Group several dynamic routes patterns into one big regex and maps |
|
| 136 | - * the routes to the pattern positions in the big regex. |
|
| 137 | - * |
|
| 138 | - * @param array $routes |
|
| 139 | - * @return array |
|
| 140 | - */ |
|
| 141 | - |
|
| 142 | - protected function buildGroup(array $routes) |
|
| 143 | - { |
|
| 144 | - $groupCount = (int) $map = $regex = []; |
|
| 145 | - |
|
| 146 | - /** @var Route $route */ |
|
| 147 | - foreach ($routes as $route) { |
|
| 148 | - $params = $route->getParams(); |
|
| 149 | - $paramsCount = count($params); |
|
| 150 | - $groupCount = max($groupCount, $paramsCount) + 1; |
|
| 151 | - $regex[] = $route->getPattern() . str_repeat("()", $groupCount - $paramsCount - 1); |
|
| 152 | - $map[$groupCount] = $route; |
|
| 153 | - } |
|
| 154 | - |
|
| 155 | - return ["~^(?|" . implode("|", $regex) . ")$~", $map]; |
|
| 156 | - } |
|
| 157 | - |
|
| 158 | - /** |
|
| 159 | - * Parse an route pattern seeking for parameters and build the route regex. |
|
| 160 | - * |
|
| 161 | - * @param string $pattern |
|
| 162 | - * @return array 0 => new route regex, 1 => map of parameter names |
|
| 163 | - */ |
|
| 164 | - |
|
| 165 | - protected function parsePlaceholders($pattern) |
|
| 166 | - { |
|
| 167 | - $params = []; |
|
| 168 | - preg_match_all("~" . Collector::DYNAMIC_REGEX . "~x", $pattern, $matches, PREG_SET_ORDER); |
|
| 169 | - |
|
| 170 | - foreach ((array) $matches as $key => $match) { |
|
| 171 | - $pattern = str_replace($match[0], isset($match[2]) ? "({$match[2]})" : "([^/]+)", $pattern); |
|
| 172 | - $params[$key] = $match[1]; |
|
| 173 | - } |
|
| 174 | - |
|
| 175 | - return [$pattern, $params]; |
|
| 176 | - } |
|
| 177 | - |
|
| 178 | - /** |
|
| 179 | - * Get only the path of a given url. |
|
| 180 | - * |
|
| 181 | - * @param string $path The given URL |
|
| 182 | - * |
|
| 183 | - * @throws Exception |
|
| 184 | - * @return string |
|
| 185 | - */ |
|
| 186 | - |
|
| 187 | - protected function parsePath($path) |
|
| 188 | - { |
|
| 189 | - $path = parse_url(substr(strstr(";" . $path, ";" . $this->basepath), strlen(";" . $this->basepath)), PHP_URL_PATH); |
|
| 190 | - |
|
| 191 | - if ($path === false) { |
|
| 192 | - throw new Exception("Seriously malformed URL passed to route matcher."); |
|
| 193 | - } |
|
| 194 | - |
|
| 195 | - return $path; |
|
| 196 | - } |
|
| 197 | - |
|
| 198 | - /** |
|
| 199 | - * Generate an HTTP error request with method not allowed or not found. |
|
| 200 | - * |
|
| 201 | - * @param string $httpMethod |
|
| 202 | - * @param string $path |
|
| 203 | - * |
|
| 204 | - * @throws NotFoundException |
|
| 205 | - * @throws MethodNotAllowedException |
|
| 206 | - */ |
|
| 207 | - |
|
| 208 | - protected function matchSimilarRoute($httpMethod, $path) |
|
| 209 | - { |
|
| 210 | - $dm = []; |
|
| 211 | - |
|
| 212 | - if (($sm = $this->checkStaticRouteInOtherMethods($httpMethod, $path)) |
|
| 213 | - || ($dm = $this->checkDynamicRouteInOtherMethods($httpMethod, $path))) { |
|
| 214 | - throw new MethodNotAllowedException($httpMethod, $path, array_merge((array) $sm, (array) $dm)); |
|
| 215 | - } |
|
| 216 | - |
|
| 217 | - throw new NotFoundException($httpMethod, $path); |
|
| 218 | - } |
|
| 219 | - |
|
| 220 | - /** |
|
| 221 | - * Verify if a static route match in another method than the requested. |
|
| 222 | - * |
|
| 223 | - * @param string $targetHttpMethod The HTTP method that must not be checked |
|
| 224 | - * @param string $path The Path that must be matched. |
|
| 225 | - * |
|
| 226 | - * @return array |
|
| 227 | - */ |
|
| 228 | - |
|
| 229 | - protected function checkStaticRouteInOtherMethods($targetHttpMethod, $path) |
|
| 230 | - { |
|
| 231 | - return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) { |
|
| 232 | - return (bool) $this->collector->findStaticRoute($httpMethod, $path); |
|
| 233 | - }); |
|
| 234 | - } |
|
| 235 | - |
|
| 236 | - /** |
|
| 237 | - * Verify if a dynamic route match in another method than the requested. |
|
| 238 | - * |
|
| 239 | - * @param string $targetHttpMethod The HTTP method that must not be checked |
|
| 240 | - * @param string $path The Path that must be matched. |
|
| 241 | - * |
|
| 242 | - * @return array |
|
| 243 | - */ |
|
| 244 | - |
|
| 245 | - protected function checkDynamicRouteInOtherMethods($targetHttpMethod, $path) |
|
| 246 | - { |
|
| 247 | - return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) { |
|
| 248 | - return (bool) $this->matchDynamicRoute($httpMethod, $path); |
|
| 249 | - }); |
|
| 250 | - } |
|
| 251 | - |
|
| 252 | - /** |
|
| 253 | - * Strip the given http methods and return all the others. |
|
| 254 | - * |
|
| 255 | - * @param array|string |
|
| 256 | - * @return array |
|
| 257 | - */ |
|
| 258 | - |
|
| 259 | - protected function getHttpMethodsBut($targetHttpMethod) |
|
| 260 | - { |
|
| 261 | - return array_diff(explode(" ", Collector::HTTP_METHODS), (array) $targetHttpMethod); |
|
| 262 | - } |
|
| 263 | - |
|
| 264 | - /** |
|
| 265 | - * @return Collector |
|
| 266 | - */ |
|
| 267 | - |
|
| 268 | - public function getCollector() |
|
| 269 | - { |
|
| 270 | - return $this->collector; |
|
| 271 | - } |
|
| 272 | - |
|
| 273 | - /** |
|
| 274 | - * @return string |
|
| 275 | - */ |
|
| 276 | - |
|
| 277 | - public function getBasePath() |
|
| 278 | - { |
|
| 279 | - return $this->basepath; |
|
| 280 | - } |
|
| 281 | - |
|
| 282 | - /** |
|
| 283 | - * Set a new basepath, this will be a prefix that must be excluded in |
|
| 284 | - * every requested Path. |
|
| 285 | - * |
|
| 286 | - * @param string $basepath The new basepath |
|
| 287 | - */ |
|
| 26 | + /** |
|
| 27 | + * @var Collector |
|
| 28 | + */ |
|
| 29 | + |
|
| 30 | + protected $collector; |
|
| 31 | + |
|
| 32 | + /** |
|
| 33 | + * Define a basepath to all routes. |
|
| 34 | + * |
|
| 35 | + * @var string |
|
| 36 | + */ |
|
| 37 | + |
|
| 38 | + protected $basepath = ""; |
|
| 39 | + |
|
| 40 | + /** |
|
| 41 | + * Construct the route dispatcher. |
|
| 42 | + * |
|
| 43 | + * @param Collector $collector |
|
| 44 | + * @param string $basepath Define a Path prefix that must be excluded on matches. |
|
| 45 | + */ |
|
| 46 | + |
|
| 47 | + public function __construct(Collector $collector, $basepath = "") |
|
| 48 | + { |
|
| 49 | + $this->collector = $collector; |
|
| 50 | + $this->basepath = $basepath; |
|
| 51 | + } |
|
| 52 | + |
|
| 53 | + /** |
|
| 54 | + * Find a route that matches the given arguments. |
|
| 55 | + * |
|
| 56 | + * @param string $httpMethod |
|
| 57 | + * @param string $path |
|
| 58 | + * |
|
| 59 | + * @throws NotFoundException |
|
| 60 | + * @throws MethodNotAllowedException |
|
| 61 | + * |
|
| 62 | + * @return Route |
|
| 63 | + */ |
|
| 64 | + |
|
| 65 | + public function match($httpMethod, $path) |
|
| 66 | + { |
|
| 67 | + $path = $this->parsePath($path); |
|
| 68 | + |
|
| 69 | + if ($route = $this->collector->findStaticRoute($httpMethod, $path)) { |
|
| 70 | + return $route; |
|
| 71 | + } |
|
| 72 | + |
|
| 73 | + if ($route = $this->matchDynamicRoute($httpMethod, $path)) { |
|
| 74 | + return $route; |
|
| 75 | + } |
|
| 76 | + |
|
| 77 | + $this->matchSimilarRoute($httpMethod, $path); |
|
| 78 | + } |
|
| 79 | + |
|
| 80 | + /** |
|
| 81 | + * Find and return the request dynamic route based on the compiled data and Path. |
|
| 82 | + * |
|
| 83 | + * @param string $httpMethod |
|
| 84 | + * @param string $path |
|
| 85 | + * |
|
| 86 | + * @return Route|false If the request match an array with the action and parameters will |
|
| 87 | + * be returned otherwise a false will. |
|
| 88 | + */ |
|
| 89 | + |
|
| 90 | + protected function matchDynamicRoute($httpMethod, $path) |
|
| 91 | + { |
|
| 92 | + if ($routes = $this->collector->findDynamicRoutes($httpMethod, $path)) { |
|
| 93 | + // chunk routes for smaller regex groups using the Sturges' Formula |
|
| 94 | + foreach (array_chunk($routes, round(1 + 3.3 * log(count($routes))), true) as $chunk) { |
|
| 95 | + array_map([$this, "buildRoute"], $chunk); |
|
| 96 | + list($pattern, $map) = $this->buildGroup($chunk); |
|
| 97 | + |
|
| 98 | + if (!preg_match($pattern, $path, $matches)) { |
|
| 99 | + continue; |
|
| 100 | + } |
|
| 101 | + |
|
| 102 | + /** @var Route $route */ |
|
| 103 | + $route = $map[count($matches)]; |
|
| 104 | + unset($matches[0]); |
|
| 105 | + |
|
| 106 | + $route->setParams(array_combine($route->getParams(), array_filter($matches))); |
|
| 107 | + $route->setMatcher($this); |
|
| 108 | + |
|
| 109 | + return $route; |
|
| 110 | + } |
|
| 111 | + } |
|
| 112 | + |
|
| 113 | + return false; |
|
| 114 | + } |
|
| 115 | + |
|
| 116 | + /** |
|
| 117 | + * Parse the dynamic segments of the pattern and replace then for |
|
| 118 | + * corresponding regex. |
|
| 119 | + * |
|
| 120 | + * @param Route $route |
|
| 121 | + * @return Route |
|
| 122 | + */ |
|
| 123 | + |
|
| 124 | + protected function buildRoute(Route $route) |
|
| 125 | + { |
|
| 126 | + if ($route->getBlock()) { |
|
| 127 | + return $route; |
|
| 128 | + } |
|
| 129 | + |
|
| 130 | + list($pattern, $params) = $this->parsePlaceholders($route->getPattern()); |
|
| 131 | + return $route->setPatternWithoutReset($pattern)->setParams($params)->setBlock(true); |
|
| 132 | + } |
|
| 133 | + |
|
| 134 | + /** |
|
| 135 | + * Group several dynamic routes patterns into one big regex and maps |
|
| 136 | + * the routes to the pattern positions in the big regex. |
|
| 137 | + * |
|
| 138 | + * @param array $routes |
|
| 139 | + * @return array |
|
| 140 | + */ |
|
| 141 | + |
|
| 142 | + protected function buildGroup(array $routes) |
|
| 143 | + { |
|
| 144 | + $groupCount = (int) $map = $regex = []; |
|
| 145 | + |
|
| 146 | + /** @var Route $route */ |
|
| 147 | + foreach ($routes as $route) { |
|
| 148 | + $params = $route->getParams(); |
|
| 149 | + $paramsCount = count($params); |
|
| 150 | + $groupCount = max($groupCount, $paramsCount) + 1; |
|
| 151 | + $regex[] = $route->getPattern() . str_repeat("()", $groupCount - $paramsCount - 1); |
|
| 152 | + $map[$groupCount] = $route; |
|
| 153 | + } |
|
| 154 | + |
|
| 155 | + return ["~^(?|" . implode("|", $regex) . ")$~", $map]; |
|
| 156 | + } |
|
| 157 | + |
|
| 158 | + /** |
|
| 159 | + * Parse an route pattern seeking for parameters and build the route regex. |
|
| 160 | + * |
|
| 161 | + * @param string $pattern |
|
| 162 | + * @return array 0 => new route regex, 1 => map of parameter names |
|
| 163 | + */ |
|
| 164 | + |
|
| 165 | + protected function parsePlaceholders($pattern) |
|
| 166 | + { |
|
| 167 | + $params = []; |
|
| 168 | + preg_match_all("~" . Collector::DYNAMIC_REGEX . "~x", $pattern, $matches, PREG_SET_ORDER); |
|
| 169 | + |
|
| 170 | + foreach ((array) $matches as $key => $match) { |
|
| 171 | + $pattern = str_replace($match[0], isset($match[2]) ? "({$match[2]})" : "([^/]+)", $pattern); |
|
| 172 | + $params[$key] = $match[1]; |
|
| 173 | + } |
|
| 174 | + |
|
| 175 | + return [$pattern, $params]; |
|
| 176 | + } |
|
| 177 | + |
|
| 178 | + /** |
|
| 179 | + * Get only the path of a given url. |
|
| 180 | + * |
|
| 181 | + * @param string $path The given URL |
|
| 182 | + * |
|
| 183 | + * @throws Exception |
|
| 184 | + * @return string |
|
| 185 | + */ |
|
| 186 | + |
|
| 187 | + protected function parsePath($path) |
|
| 188 | + { |
|
| 189 | + $path = parse_url(substr(strstr(";" . $path, ";" . $this->basepath), strlen(";" . $this->basepath)), PHP_URL_PATH); |
|
| 190 | + |
|
| 191 | + if ($path === false) { |
|
| 192 | + throw new Exception("Seriously malformed URL passed to route matcher."); |
|
| 193 | + } |
|
| 194 | + |
|
| 195 | + return $path; |
|
| 196 | + } |
|
| 197 | + |
|
| 198 | + /** |
|
| 199 | + * Generate an HTTP error request with method not allowed or not found. |
|
| 200 | + * |
|
| 201 | + * @param string $httpMethod |
|
| 202 | + * @param string $path |
|
| 203 | + * |
|
| 204 | + * @throws NotFoundException |
|
| 205 | + * @throws MethodNotAllowedException |
|
| 206 | + */ |
|
| 207 | + |
|
| 208 | + protected function matchSimilarRoute($httpMethod, $path) |
|
| 209 | + { |
|
| 210 | + $dm = []; |
|
| 211 | + |
|
| 212 | + if (($sm = $this->checkStaticRouteInOtherMethods($httpMethod, $path)) |
|
| 213 | + || ($dm = $this->checkDynamicRouteInOtherMethods($httpMethod, $path))) { |
|
| 214 | + throw new MethodNotAllowedException($httpMethod, $path, array_merge((array) $sm, (array) $dm)); |
|
| 215 | + } |
|
| 216 | + |
|
| 217 | + throw new NotFoundException($httpMethod, $path); |
|
| 218 | + } |
|
| 219 | + |
|
| 220 | + /** |
|
| 221 | + * Verify if a static route match in another method than the requested. |
|
| 222 | + * |
|
| 223 | + * @param string $targetHttpMethod The HTTP method that must not be checked |
|
| 224 | + * @param string $path The Path that must be matched. |
|
| 225 | + * |
|
| 226 | + * @return array |
|
| 227 | + */ |
|
| 228 | + |
|
| 229 | + protected function checkStaticRouteInOtherMethods($targetHttpMethod, $path) |
|
| 230 | + { |
|
| 231 | + return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) { |
|
| 232 | + return (bool) $this->collector->findStaticRoute($httpMethod, $path); |
|
| 233 | + }); |
|
| 234 | + } |
|
| 235 | + |
|
| 236 | + /** |
|
| 237 | + * Verify if a dynamic route match in another method than the requested. |
|
| 238 | + * |
|
| 239 | + * @param string $targetHttpMethod The HTTP method that must not be checked |
|
| 240 | + * @param string $path The Path that must be matched. |
|
| 241 | + * |
|
| 242 | + * @return array |
|
| 243 | + */ |
|
| 244 | + |
|
| 245 | + protected function checkDynamicRouteInOtherMethods($targetHttpMethod, $path) |
|
| 246 | + { |
|
| 247 | + return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) { |
|
| 248 | + return (bool) $this->matchDynamicRoute($httpMethod, $path); |
|
| 249 | + }); |
|
| 250 | + } |
|
| 251 | + |
|
| 252 | + /** |
|
| 253 | + * Strip the given http methods and return all the others. |
|
| 254 | + * |
|
| 255 | + * @param array|string |
|
| 256 | + * @return array |
|
| 257 | + */ |
|
| 258 | + |
|
| 259 | + protected function getHttpMethodsBut($targetHttpMethod) |
|
| 260 | + { |
|
| 261 | + return array_diff(explode(" ", Collector::HTTP_METHODS), (array) $targetHttpMethod); |
|
| 262 | + } |
|
| 263 | + |
|
| 264 | + /** |
|
| 265 | + * @return Collector |
|
| 266 | + */ |
|
| 267 | + |
|
| 268 | + public function getCollector() |
|
| 269 | + { |
|
| 270 | + return $this->collector; |
|
| 271 | + } |
|
| 272 | + |
|
| 273 | + /** |
|
| 274 | + * @return string |
|
| 275 | + */ |
|
| 276 | + |
|
| 277 | + public function getBasePath() |
|
| 278 | + { |
|
| 279 | + return $this->basepath; |
|
| 280 | + } |
|
| 281 | + |
|
| 282 | + /** |
|
| 283 | + * Set a new basepath, this will be a prefix that must be excluded in |
|
| 284 | + * every requested Path. |
|
| 285 | + * |
|
| 286 | + * @param string $basepath The new basepath |
|
| 287 | + */ |
|
| 288 | 288 | |
| 289 | - public function setBasePath($basepath) |
|
| 290 | - { |
|
| 291 | - $this->basepath = $basepath; |
|
| 292 | - } |
|
| 289 | + public function setBasePath($basepath) |
|
| 290 | + { |
|
| 291 | + $this->basepath = $basepath; |
|
| 292 | + } |
|
| 293 | 293 | |
| 294 | 294 | } |