xiaowang/kirby/src/Http/Router.php
2023-04-14 16:30:28 +02:00

198 lines
4.2 KiB
PHP

<?php
namespace Kirby\Http;
use Closure;
use Exception;
use InvalidArgumentException;
use Kirby\Toolkit\A;
/**
* @package Kirby Http
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*/
class Router
{
/**
* Hook that is called after each route
*/
protected Closure|null $afterEach;
/**
* Hook that is called before each route
*/
protected Closure|null $beforeEach;
/**
* Store for the current route,
* if one can be found
*/
protected Route|null $route = null;
/**
* All registered routes, sorted by
* their request method. This makes
* it faster to find the right route
* later.
*/
protected array $routes = [
'GET' => [],
'HEAD' => [],
'POST' => [],
'PUT' => [],
'DELETE' => [],
'CONNECT' => [],
'OPTIONS' => [],
'TRACE' => [],
'PATCH' => [],
];
/**
* Creates a new router object and
* registers all the given routes
*
* @param array<string, \Closure> $hooks Optional `beforeEach` and `afterEach` hooks
*/
public function __construct(array $routes = [], array $hooks = [])
{
$this->beforeEach = $hooks['beforeEach'] ?? null;
$this->afterEach = $hooks['afterEach'] ?? null;
foreach ($routes as $props) {
if (isset($props['pattern'], $props['action']) === false) {
throw new InvalidArgumentException('Invalid route parameters');
}
$patterns = A::wrap($props['pattern']);
$methods = A::map(
explode('|', strtoupper($props['method'] ?? 'GET')),
'trim'
);
if ($methods === ['ALL']) {
$methods = array_keys($this->routes);
}
foreach ($methods as $method) {
foreach ($patterns as $pattern) {
$this->routes[$method][] = new Route(
$pattern,
$method,
$props['action'],
$props
);
}
}
}
}
/**
* Calls the Router by path and method.
* This will try to find a Route object
* and then call the Route action with
* the appropriate arguments and a Result
* object.
*/
public function call(
string|null $path = null,
string $method = 'GET',
Closure|null $callback = null
) {
$path ??= '';
$ignore = [];
$result = null;
$loop = true;
while ($loop === true) {
$route = $this->find($path, $method, $ignore);
if ($this->beforeEach instanceof Closure) {
($this->beforeEach)($route, $path, $method);
}
try {
if ($callback) {
$result = $callback($route);
} else {
$result = $route->action()->call(
$route,
...$route->arguments()
);
}
$loop = false;
} catch (Exceptions\NextRouteException) {
$ignore[] = $route;
}
if ($this->afterEach instanceof Closure) {
$final = $loop === false;
$result = ($this->afterEach)($route, $path, $method, $result, $final);
}
}
return $result;
}
/**
* Creates a micro-router and executes
* the routing action immediately
* @since 3.7.0
*/
public static function execute(
string|null $path = null,
string $method = 'GET',
array $routes = [],
Closure|null $callback = null
) {
return (new static($routes))->call($path, $method, $callback);
}
/**
* Finds a Route object by path and method
* The Route's arguments method is used to
* find matches and return all the found
* arguments in the path.
*/
public function find(
string $path,
string $method,
array|null $ignore = null
): Route {
if (isset($this->routes[$method]) === false) {
throw new InvalidArgumentException('Invalid routing method: ' . $method, 400);
}
// remove leading and trailing slashes
$path = trim($path, '/');
foreach ($this->routes[$method] as $route) {
$arguments = $route->parse($route->pattern(), $path);
if ($arguments !== false) {
if (
empty($ignore) === true ||
in_array($route, $ignore) === false
) {
return $this->route = $route;
}
}
}
throw new Exception('No route found for path: "' . $path . '" and request method: "' . $method . '"', 404);
}
/**
* Returns the current route.
* This will only return something,
* once Router::find() has been called
* and only if a route was found.
*/
public function route(): Route|null
{
return $this->route;
}
}