* @license MIT, http://flightphp.com/license */ namespace flight\net; use Exception; use flight\net\Route; /** * The Router class is responsible for routing an HTTP request to * an assigned callback function. The Router tries to match the * requested URL against a series of URL patterns. */ class Router { /** * Case sensitive matching. */ public bool $case_sensitive = false; /** * Mapped routes. * @var array */ protected array $routes = []; /** * Pointer to current route. */ protected int $index = 0; /** * When groups are used, this is mapped against all the routes * * @var string */ protected string $group_prefix = ''; /** * Group Middleware * * @var array */ protected array $group_middlewares = []; /** * Gets mapped routes. * * @return array Array of routes */ public function getRoutes(): array { return $this->routes; } /** * Clears all routes in the router. */ public function clear(): void { $this->routes = []; } /** * Maps a URL pattern to a callback function. * * @param string $pattern URL pattern to match * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $route_alias Alias for the route * @return Route */ public function map(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): Route { $url = trim($pattern); $methods = ['*']; if (false !== strpos($url, ' ')) { [$method, $url] = explode(' ', $url, 2); $url = trim($url); $methods = explode('|', $method); } $route = new Route($this->group_prefix.$url, $callback, $methods, $pass_route, $route_alias); // to handle group middleware foreach($this->group_middlewares as $gm) { $route->addMiddleware($gm); } $this->routes[] = $route; return $route; } /** * Creates a GET based route * * @param string $pattern URL pattern to match * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route * @return Route */ public function get(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('GET ' . $pattern, $callback, $pass_route, $alias); } /** * Creates a POST based route * * @param string $pattern URL pattern to match * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route * @return Route */ public function post(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('POST ' . $pattern, $callback, $pass_route, $alias); } /** * Creates a PUT based route * * @param string $pattern URL pattern to match * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route * @return Route */ public function put(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('PUT ' . $pattern, $callback, $pass_route, $alias); } /** * Creates a PATCH based route * * @param string $pattern URL pattern to match * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route * @return Route */ public function patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('PATCH ' . $pattern, $callback, $pass_route, $alias); } /** * Creates a DELETE based route * * @param string $pattern URL pattern to match * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route * @return Route */ public function delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('DELETE ' . $pattern, $callback, $pass_route, $alias); } /** * Group together a set of routes * * @param string $group_prefix group URL prefix (such as /api/v1) * @param callable $callback The necessary calling that holds the Router class * @param array $middlewares The middlewares to be applied to the group Ex: [ $middleware1, $middleware2 ] * @return void */ public function group(string $group_prefix, callable $callback, array $group_middlewares = []): void { $old_group_prefix = $this->group_prefix; $old_group_middlewares = $this->group_middlewares; $this->group_prefix .= $group_prefix; $this->group_middlewares = array_merge($this->group_middlewares, $group_middlewares); $callback($this); $this->group_prefix = $old_group_prefix; $this->group_middlewares = $old_group_middlewares; } /** * Routes the current request. * * @param Request $request Request object * * @return bool|Route Matching route or false if no match */ public function route(Request $request) { $url_decoded = urldecode($request->url); while ($route = $this->current()) { if ($route->matchMethod($request->method) && $route->matchUrl($url_decoded, $this->case_sensitive)) { return $route; } $this->next(); } return false; } /** * Gets the URL for a given route alias * * @param string $alias the alias to match * @param array $params the parameters to pass to the route * @return string */ public function getUrlByAlias(string $alias, array $params = []): string { $potential_aliases = []; foreach($this->routes as $route) { $potential_aliases[] = $route->alias; if ($route->matchAlias($alias)) { return $route->hydrateUrl($params); } } // use a levenshtein to find the closest match and make a recommendation $closest_match = ''; $closest_match_distance = 0; foreach($potential_aliases as $potential_alias) { $levenshtein_distance = levenshtein($alias, $potential_alias); if($levenshtein_distance > $closest_match_distance) { $closest_match = $potential_alias; $closest_match_distance = $levenshtein_distance; } } $exception_message = 'No route found with alias: \'' . $alias . '\'.'; if($closest_match !== '') { $exception_message .= ' Did you mean \'' . $closest_match . '\'?'; } throw new Exception($exception_message); } /** * Rewinds the current route index. */ public function rewind(): void { $this->index = 0; } /** * Checks if more routes can be iterated. * * @return bool More routes */ public function valid(): bool { return isset($this->routes[$this->index]); } /** * Gets the current route. * * @return bool|Route */ public function current() { return $this->routes[$this->index] ?? false; } /** * Gets the next route. */ public function next(): void { $this->index++; } /** * Reset to the first route. */ public function reset(): void { $this->index = 0; } }