*/ class Route { /** * URL pattern */ public string $pattern; /** * Callback function * * @var mixed */ public $callback; /** * HTTP methods * * @var array */ public array $methods = []; /** * Route parameters * * @var array */ public array $params = []; /** * Matching regular expression */ public ?string $regex = null; /** * URL splat content */ public string $splat = ''; /** * Pass self in callback parameters */ public bool $pass = false; /** * The alias is a way to identify the route using a simple name ex: 'login' instead of /admin/login */ public string $alias = ''; /** * The middleware to be applied to the route * * @var array */ public array $middleware = []; /** Whether the response for this route should be streamed. */ public bool $is_streamed = false; /** * If this route is streamed, the headers to be sent before the response. * * @var array */ public array $streamed_headers = []; /** * Constructor. * * @param string $pattern URL pattern * @param callable|string $callback Callback function * @param array $methods HTTP methods * @param bool $pass Pass self in callback parameters */ public function __construct(string $pattern, $callback, array $methods, bool $pass, string $alias = '') { $this->pattern = $pattern; $this->callback = $callback; $this->methods = $methods; $this->pass = $pass; $this->alias = $alias; } /** * Checks if a URL matches the route pattern. Also parses named parameters in the URL. * * @param string $url Requested URL (original format, not URL decoded) * @param bool $case_sensitive Case sensitive matching * * @return bool Match status */ public function matchUrl(string $url, bool $case_sensitive = false): bool { // Wildcard or exact match if ($this->pattern === '*' || $this->pattern === $url) { return true; } $ids = []; $last_char = substr($this->pattern, -1); // Get splat if ($last_char === '*') { $n = 0; $len = \strlen($url); $count = substr_count($this->pattern, '/'); for ($i = 0; $i < $len; $i++) { if ($url[$i] === '/') { ++$n; } if ($n === $count) { break; } } $this->splat = urldecode(strval(substr($url, $i + 1))); } // Build the regex for matching $pattern_utf_chars_encoded = preg_replace_callback( '#(\\p{L}+)#u', static function ($matches) { return urlencode($matches[0]); }, $this->pattern ); $regex = str_replace([')', '/*'], [')?', '(/?|/.*?)'], $pattern_utf_chars_encoded); $regex = preg_replace_callback( '#@([\w]+)(:([^/\(\)]*))?#', static function ($matches) use (&$ids) { $ids[$matches[1]] = null; if (isset($matches[3])) { return '(?P<' . $matches[1] . '>' . $matches[3] . ')'; } return '(?P<' . $matches[1] . '>[^/\?]+)'; }, $regex ); $regex .= $last_char === '/' ? '?' : '/?'; // Attempt to match route and named parameters if (!preg_match('#^' . $regex . '(?:\?[\s\S]*)?$#' . (($case_sensitive) ? '' : 'i'), $url, $matches)) { return false; } foreach (array_keys($ids) as $k) { $this->params[$k] = (\array_key_exists($k, $matches)) ? urldecode($matches[$k]) : null; } $this->regex = $regex; return true; } /** * Checks if an HTTP method matches the route methods. * * @param string $method HTTP method * * @return bool Match status */ public function matchMethod(string $method): bool { return \count(array_intersect([$method, '*'], $this->methods)) > 0; } /** * Checks if an alias matches the route alias. */ public function matchAlias(string $alias): bool { return $this->alias === $alias; } /** * Hydrates the route url with the given parameters * * @param array $params the parameters to pass to the route */ public function hydrateUrl(array $params = []): string { $url = preg_replace_callback("/(?:@([\w]+)(?:\:([^\/]+))?\)*)/i", function ($match) use ($params) { if (isset($match[1]) && isset($params[$match[1]])) { return $params[$match[1]]; } }, $this->pattern); // catches potential optional parameter $url = str_replace('(/', '/', $url); // trim any trailing slashes if ($url !== '/') { $url = rtrim($url, '/'); } return $url; } /** * Sets the route alias * * @return $this */ public function setAlias(string $alias): self { $this->alias = $alias; return $this; } /** * Sets the route middleware * * @param array|callable|string $middleware */ public function addMiddleware($middleware): self { if (is_array($middleware) === true) { $this->middleware = array_merge($this->middleware, $middleware); } else { $this->middleware[] = $middleware; } return $this; } /** * If the response should be streamed * * @return self */ public function stream(): self { $this->is_streamed = true; return $this; } /** * This will allow the response for this route to be streamed. * * @param array $headers a key value of headers to set before the stream starts. * * @return $this */ public function streamWithHeaders(array $headers): self { $this->is_streamed = true; $this->streamed_headers = $headers; return $this; } }