<?php
namespace Site\Lib;
class Route {
protected static array $routes = [];
protected static array $fallback = [];
/**
* ルート設定を言語固有のハンドラで初期化する
*
* @param array $routes ルート設定の配列
* @return void
*/
public static function init(array $routes): void {
self::$routes = $routes;
}
/**
* ルートを追加する
*
* @param string $method HTTPメソッド
* @param string $path URLパス
* @param string|callable $class ハンドラクラスとメソッド、またはコールバック
* @param array $params オプションのパラメータ
* @return array ルート設定
*/
public static function add(string $method, string $path, string|callable $class,
array $params = []): array {
$route = [
'method' => $method,
'path' => $path,
'class' => $class,
'params' => $params,
];
self::$routes[] = $route;
return $route;
}
/**
* 404処理用のフォールバックルートを設定する
*
* @param array|string|callable $class
* @return void
*/
public static function setFallback(array|string|callable $class): void {
self::$fallback = [
'class' => $class,
'params' => [],
];
}
/**
* 適切なルートをマッチさせて実行する
*
* @param string $uri リクエストURI
* @return void
*/
public static function dispatch(string $uri): void {
// URIをパスとクエリ文字列に分割
$uriParts = explode('?', $uri, 2);
$path = trim($uriParts[0], " \t\n\r\0\x0B/");
// ルートパスの処理(/?page=2のようなクエリパラメータを含む場合も処理)
if ($path === '') {
self::executeClass([
'class' => [new \Site\Controller\Home(), 'show'],
'params' => ['lang' => 'ja'],
]);
return;
}
if ($path === 'en') {
self::executeClass([
'class' => [new \Site\Controller\Home(), 'show'],
'params' => ['lang' => 'en'],
]);
return;
}
// パスに対してルートをマッチングする
foreach (self::$routes as $route) {
$matches = [];
if (self::matchRoute($route['path'], $path, $matches)) {
$params = self::extractParams($route['path'], $path);
$params = array_merge($route['params'], $params);
if (is_string($route['class'])) {
[ $class, $method ] = explode('@', $route['class']);
$controller = new $class();
self::executeClass([
'class' => [ $controller, $method ],
'params' => $params,
]);
return;
} elseif (is_callable($route['class'])) {
self::executeClass([
'class' => $route['class'],
'params' => $params,
]);
return;
}
}
}
// マッチするルートがない場合、フォールバックを実行
self::executeClass(self::$fallback);
}
/**
* ルートパターンとパスをマッチングする
*
* @param string $pattern ルートパターン
* @param string $path 現在のパス
* @param array $matches マッチを格納する参照
* @return bool
*/
protected static function matchRoute(string $pattern, string $path,
array &$matches = []): bool {
// ルートパターンを正規表現パターンに変換
$pattern = preg_replace('/\{([^:}]+)(?::([^}]+))?\}/', '(?P<$1>[^/]+)', $pattern);
$pattern = str_replace('/', '\/', $pattern);
return (bool)preg_match('/^'.$pattern.'$/', $path, $matches);
}
/**
* パターンに基づいてパスから名前付きパラメータを抽出する
*
* @param string $pattern ルートパターン
* @param string $path 現在のパス
* @return array
*/
protected static function extractParams(string $pattern, string $path): array {
$params = [];
$patternParts = explode('/', $pattern);
$pathParts = explode('/', $path);
foreach ($patternParts as $k => $v) {
if (preg_match('/\{([^:}]+)(?::([^}]+))?\}/', $v, $matches)) {
if (isset($pathParts[$k])) {
$params[$matches[1]] = $pathParts[$k];
}
}
}
return $params;
}
/**
* ルートクラスを実行する
*
* @param array $route ルート設定
* @return void
*/
protected static function executeClass(array $route): void {
if (is_callable($route['class'])) {
call_user_func($route['class'], $route['params'] ?? []);
}
}
}