mirror of
https://github.com/mivodev/mivo.git
synced 2026-01-26 05:25:42 +07:00
210 lines
6.5 KiB
PHP
210 lines
6.5 KiB
PHP
<?php
|
|
|
|
namespace App\Core;
|
|
|
|
class Router {
|
|
protected $routes = [];
|
|
protected $currentGroupMiddleware = [];
|
|
protected $lastRouteKey = null;
|
|
|
|
protected $middlewareAliases = [
|
|
'auth' => \App\Middleware\AuthMiddleware::class,
|
|
'cors' => \App\Middleware\CorsMiddleware::class,
|
|
'router.valid' => \App\Middleware\RouterCheckMiddleware::class,
|
|
];
|
|
|
|
/**
|
|
* Add a GET route
|
|
*/
|
|
public function get($path, $callback) {
|
|
return $this->addRoute('GET', $path, $callback);
|
|
}
|
|
|
|
/**
|
|
* Add a POST route
|
|
*/
|
|
public function post($path, $callback) {
|
|
return $this->addRoute('POST', $path, $callback);
|
|
}
|
|
|
|
/**
|
|
* Add a OPTIONS route (Crucial for CORS Preflight)
|
|
*/
|
|
public function options($path, $callback) {
|
|
return $this->addRoute('OPTIONS', $path, $callback);
|
|
}
|
|
|
|
/**
|
|
* Add a PUT route
|
|
*/
|
|
public function put($path, $callback) {
|
|
return $this->addRoute('PUT', $path, $callback);
|
|
}
|
|
|
|
/**
|
|
* Add a PATCH route
|
|
*/
|
|
public function patch($path, $callback) {
|
|
return $this->addRoute('PATCH', $path, $callback);
|
|
}
|
|
|
|
/**
|
|
* Add a DELETE route
|
|
*/
|
|
public function delete($path, $callback) {
|
|
return $this->addRoute('DELETE', $path, $callback);
|
|
}
|
|
|
|
/**
|
|
* Add route to collection and return $this for chaining
|
|
*/
|
|
protected function addRoute($method, $path, $callback) {
|
|
$path = $this->normalizePath($path);
|
|
|
|
$this->routes[$method][$path] = [
|
|
'callback' => $callback,
|
|
'middleware' => $this->currentGroupMiddleware // Inherit group middleware
|
|
];
|
|
|
|
$this->lastRouteKey = ['method' => $method, 'path' => $path];
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Attach middleware to the last defined route
|
|
*/
|
|
public function middleware($names) {
|
|
if (!$this->lastRouteKey) return $this;
|
|
|
|
$method = $this->lastRouteKey['method'];
|
|
$path = $this->lastRouteKey['path'];
|
|
|
|
$middlewares = is_array($names) ? $names : [$names];
|
|
|
|
// Merge with existing middleware (from groups)
|
|
$this->routes[$method][$path]['middleware'] = array_merge(
|
|
$this->routes[$method][$path]['middleware'],
|
|
$middlewares
|
|
);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Define a route group with shared attributes (middleware, prefix, etc.)
|
|
*/
|
|
public function group($attributes, callable $callback) {
|
|
$previousGroupMiddleware = $this->currentGroupMiddleware;
|
|
|
|
if (isset($attributes['middleware'])) {
|
|
$newMiddleware = is_array($attributes['middleware'])
|
|
? $attributes['middleware']
|
|
: [$attributes['middleware']];
|
|
|
|
$this->currentGroupMiddleware = array_merge(
|
|
$this->currentGroupMiddleware,
|
|
$newMiddleware
|
|
);
|
|
}
|
|
|
|
// Execute the callback with $this router instance
|
|
$callback($this);
|
|
|
|
// Restore previous state
|
|
$this->currentGroupMiddleware = $previousGroupMiddleware;
|
|
}
|
|
|
|
protected function normalizePath($path) {
|
|
return '/' . trim($path, '/');
|
|
}
|
|
|
|
public function dispatch($uri, $method) {
|
|
// Fire hook to allow plugins to register routes
|
|
\App\Core\Hooks::doAction('router_init', $this);
|
|
|
|
$path = parse_url($uri, PHP_URL_PATH);
|
|
|
|
// Handle subdirectory (SKIP for PHP Built-in Server to avoid SCRIPT_NAME issues)
|
|
if (php_sapi_name() !== 'cli-server') {
|
|
$scriptName = dirname($_SERVER['SCRIPT_NAME']);
|
|
// Normalize backslashes (Windows)
|
|
$scriptName = str_replace('\\', '/', $scriptName);
|
|
|
|
// Ensure we don't strip root slash
|
|
if ($scriptName !== '/' && strpos($path, $scriptName) === 0) {
|
|
$path = substr($path, strlen($scriptName));
|
|
}
|
|
}
|
|
$path = $this->normalizePath($path);
|
|
|
|
// Global Install Check
|
|
$dbPath = ROOT . '/app/Database/database.sqlite';
|
|
if (!file_exists($dbPath)) {
|
|
if ($path !== '/install' && strpos($path, '/assets/') !== 0) {
|
|
header('Location: /install');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// 1. Try Exact Match
|
|
if (isset($this->routes[$method][$path])) {
|
|
return $this->runRoute($this->routes[$method][$path], []);
|
|
}
|
|
|
|
// 2. Try Dynamic Routes (Regex)
|
|
foreach ($this->routes[$method] as $route => $config) {
|
|
// e.g. /dashboard/{session} -> #^/dashboard/([^/]+)$#
|
|
$pattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '([^/]+)', $route);
|
|
$pattern = "#^" . $pattern . "$#";
|
|
|
|
if (preg_match($pattern, $path, $matches)) {
|
|
array_shift($matches); // Remove full match
|
|
$matches = array_map('urldecode', $matches);
|
|
return $this->runRoute($config, $matches);
|
|
}
|
|
}
|
|
|
|
\App\Helpers\ErrorHelper::show(404);
|
|
}
|
|
|
|
protected function runRoute($routeConfig, $params) {
|
|
$callback = $routeConfig['callback'];
|
|
$middlewares = $routeConfig['middleware'];
|
|
|
|
// Pipeline Runner
|
|
$pipeline = array_reduce(
|
|
array_reverse($middlewares),
|
|
function ($nextStack, $middlewareName) {
|
|
return function ($request) use ($nextStack, $middlewareName) {
|
|
// Resolve Middleware Class
|
|
$class = $this->middlewareAliases[$middlewareName] ?? $middlewareName;
|
|
|
|
if (!class_exists($class)) {
|
|
throw new \Exception("Middleware class '$class' not found.");
|
|
}
|
|
|
|
$instance = new $class();
|
|
return $instance->handle($request, $nextStack);
|
|
};
|
|
},
|
|
function ($request) use ($callback, $params) {
|
|
// Final destination: The Controller
|
|
return $this->invokeCallback($callback, $params);
|
|
}
|
|
);
|
|
|
|
// Start the pipeline with the current request (mock object or just null/path)
|
|
return $pipeline($_SERVER);
|
|
}
|
|
|
|
protected function invokeCallback($callback, $params = []) {
|
|
if (is_array($callback)) {
|
|
$controller = new $callback[0]();
|
|
$method = $callback[1];
|
|
return call_user_func_array([$controller, $method], $params);
|
|
}
|
|
return call_user_func_array($callback, $params);
|
|
}
|
|
}
|