193 lines
4.0 KiB
PHP
193 lines
4.0 KiB
PHP
<?php
|
|
|
|
namespace Kirby\Http;
|
|
|
|
use Closure;
|
|
|
|
/**
|
|
* @package Kirby Http
|
|
* @author Bastian Allgeier <bastian@getkirby.com>
|
|
* @link https://getkirby.com
|
|
* @copyright Bastian Allgeier
|
|
* @license https://opensource.org/licenses/MIT
|
|
*/
|
|
class Route
|
|
{
|
|
/**
|
|
* The callback action function
|
|
*/
|
|
protected Closure $action;
|
|
|
|
/**
|
|
* Listed of parsed arguments
|
|
*/
|
|
protected array $arguments = [];
|
|
|
|
/**
|
|
* An array of all passed attributes
|
|
*/
|
|
protected array $attributes = [];
|
|
|
|
/**
|
|
* The registered request method
|
|
*/
|
|
protected string $method;
|
|
|
|
/**
|
|
* The registered pattern
|
|
*/
|
|
protected string $pattern;
|
|
|
|
/**
|
|
* Wildcards, which can be used in
|
|
* Route patterns to make regular expressions
|
|
* a little more human
|
|
*/
|
|
protected array $wildcards = [
|
|
'required' => [
|
|
'(:num)' => '(-?[0-9]+)',
|
|
'(:alpha)' => '([a-zA-Z]+)',
|
|
'(:alphanum)' => '([a-zA-Z0-9]+)',
|
|
'(:any)' => '([a-zA-Z0-9\.\-_%= \+\@\(\)]+)',
|
|
'(:all)' => '(.*)',
|
|
],
|
|
'optional' => [
|
|
'/(:num?)' => '(?:/(-?[0-9]+)',
|
|
'/(:alpha?)' => '(?:/([a-zA-Z]+)',
|
|
'/(:alphanum?)' => '(?:/([a-zA-Z0-9]+)',
|
|
'/(:any?)' => '(?:/([a-zA-Z0-9\.\-_%= \+\@\(\)]+)',
|
|
'/(:all?)' => '(?:/(.*)',
|
|
],
|
|
];
|
|
|
|
/**
|
|
* Magic getter for route attributes
|
|
*/
|
|
public function __call(string $key, array $args = null): mixed
|
|
{
|
|
return $this->attributes[$key] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Creates a new Route object for the given
|
|
* pattern(s), method(s) and the callback action
|
|
*/
|
|
public function __construct(
|
|
string $pattern,
|
|
string $method,
|
|
Closure $action,
|
|
array $attributes = []
|
|
) {
|
|
$this->action = $action;
|
|
$this->attributes = $attributes;
|
|
$this->method = $method;
|
|
$this->pattern = $this->regex(ltrim($pattern, '/'));
|
|
}
|
|
|
|
/**
|
|
* Getter for the action callback
|
|
*/
|
|
public function action(): Closure
|
|
{
|
|
return $this->action;
|
|
}
|
|
|
|
/**
|
|
* Returns all parsed arguments
|
|
*/
|
|
public function arguments(): array
|
|
{
|
|
return $this->arguments;
|
|
}
|
|
|
|
/**
|
|
* Getter for additional attributes
|
|
*/
|
|
public function attributes(): array
|
|
{
|
|
return $this->attributes;
|
|
}
|
|
|
|
/**
|
|
* Getter for the method
|
|
*/
|
|
public function method(): string
|
|
{
|
|
return $this->method;
|
|
}
|
|
|
|
/**
|
|
* Returns the route name if set
|
|
*/
|
|
public function name(): string|null
|
|
{
|
|
return $this->attributes['name'] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Throws a specific exception to tell
|
|
* the router to jump to the next route
|
|
* @since 3.0.3
|
|
*/
|
|
public static function next(): void
|
|
{
|
|
throw new Exceptions\NextRouteException('next');
|
|
}
|
|
|
|
/**
|
|
* Getter for the pattern
|
|
*/
|
|
public function pattern(): string
|
|
{
|
|
return $this->pattern;
|
|
}
|
|
|
|
/**
|
|
* Converts the pattern into a full regular
|
|
* expression by replacing all the wildcards
|
|
*/
|
|
public function regex(string $pattern): string
|
|
{
|
|
$search = array_keys($this->wildcards['optional']);
|
|
$replace = array_values($this->wildcards['optional']);
|
|
|
|
// For optional parameters, first translate the wildcards to their
|
|
// regex equivalent, sans the ")?" ending. We'll add the endings
|
|
// back on when we know the replacement count.
|
|
$pattern = str_replace($search, $replace, $pattern, $count);
|
|
|
|
if ($count > 0) {
|
|
$pattern .= str_repeat(')?', $count);
|
|
}
|
|
|
|
return strtr($pattern, $this->wildcards['required']);
|
|
}
|
|
|
|
/**
|
|
* Tries to match the path with the regular expression and
|
|
* extracts all arguments for the Route action
|
|
*/
|
|
public function parse(string $pattern, string $path): array|false
|
|
{
|
|
// check for direct matches
|
|
if ($pattern === $path) {
|
|
return $this->arguments = [];
|
|
}
|
|
|
|
// We only need to check routes with regular expression since all others
|
|
// would have been able to be matched by the search for literal matches
|
|
// we just did before we started searching.
|
|
if (strpos($pattern, '(') === false) {
|
|
return false;
|
|
}
|
|
|
|
// If we have a match we'll return all results
|
|
// from the preg without the full first match.
|
|
if (preg_match('#^' . $this->regex($pattern) . '$#u', $path, $parameters)) {
|
|
return $this->arguments = array_slice($parameters, 1);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|