dbms_primeidc/extdbms/lib/Core/Router.php
2025-04-23 19:28:58 +09:00

133 lines
4.3 KiB
PHP

<?php
namespace lib\Core;
use lib\Core\Http\Response;
class Router
{
private array $routes = [];
private string $currentGroupPrefix = '';
private array $currentGroupMiddlewares = [];
/**
* 라우트 등록 (동적 파라미터 지원)
*/
public function add(string $method, string $path, callable $callback, array $middlewares = []): void
{
// 그룹 접두사와 병합
$fullPath = trim($this->currentGroupPrefix . '/' . trim($path, '/'), '/');
$route = [
'method' => strtoupper($method),
'path' => $fullPath,
'callback' => $callback,
'middlewares' => array_merge($this->currentGroupMiddlewares, $middlewares)
];
// 동적 파라미터({param})가 있으면 정규식 패턴 생성
if (strpos($fullPath, '{') !== false) {
$pattern = preg_replace_callback('/\{(\w+)\}/', function ($matches) {
return '(?P<' . $matches[1] . '>[^/]+)';
}, $fullPath);
$route['pattern'] = '/^' . str_replace('/', '\/', $pattern) . '(\/.*)?$/';
} else {
// SEO key/value 형태를 위해 접두사 이후의 추가 파라미터를 허용
$route['pattern'] = '/^' . str_replace('/', '\/', $fullPath) . '(\/(?P<extra>.*))?$/';
}
$this->routes[] = $route;
}
/**
* 라우트 그룹: 공통 접두사와 공통 미들웨어를 적용
*/
public function group(string $prefix, callable $callback, array $middlewares = []): void
{
$previousGroupPrefix = $this->currentGroupPrefix;
$previousGroupMiddlewares = $this->currentGroupMiddlewares;
$this->currentGroupPrefix = trim($previousGroupPrefix . '/' . trim($prefix, '/'), '/');
$this->currentGroupMiddlewares = array_merge($previousGroupMiddlewares, $middlewares);
$callback($this);
$this->currentGroupPrefix = $previousGroupPrefix;
$this->currentGroupMiddlewares = $previousGroupMiddlewares;
}
/**
* 라우트 매칭 및 실행
*/
public function dispatch(string $uri, string $method = 'GET'): mixed
{
$uri = trim(parse_url($uri, PHP_URL_PATH), '/');
// 정적 파일 처리: public 폴더 내에 파일이 있으면 직접 서빙
$staticFile = DOCUMENTROOT_PATH . $uri;
if (file_exists($staticFile) && is_file($staticFile)) {
return $this->serveStaticFile($staticFile);
}
foreach ($this->routes as $route) {
if ($route['method'] !== strtoupper($method)) { //GET, POST 등 HTTP 메소드 체크
continue;
}
if (preg_match($route['pattern'], $uri, $matches)) {
$params = [];
// 패턴에 named 그룹이 있다면 추출
foreach ($matches as $key => $value) {
if (is_string($key)) {
$params[$key] = $value;
}
}
//추가 SEO key/value 파라미터로 파싱
if (isset($params['extra']) && $params['extra'] !== '') {
$extra = trim($params['extra'], '/');
$extraSegments = explode('/', $extra);
// 짝수개여야 key/value 쌍으로 변환 가능
if (count($extraSegments) % 2 === 0) {
for ($i = 0; $i < count($extraSegments); $i += 2) {
$key = $extraSegments[$i];
$value = $extraSegments[$i + 1];
$params[$key] = $value;
}
}
unset($params['extra']);
} else {
// 패턴에 extra 그룹이 없으면, 기본 GET 파라미터와 병합
$params = array_merge($_GET, $params);
}
return $this->handle($route, $params);
}
}
// 404 응답 객체 반환
return (new Response('해당 Route 경로를 찾을수 없습니다.' . $uri, 404));
}
/**
* 미들웨어 체인을 거쳐 최종 콜백 실행
*/
private function handle(array $route, array $params): mixed
{
$handler = $route['callback'];
$middlewares = $route['middlewares'];
$pipeline = array_reduce(
array_reverse($middlewares),
fn($next, $middleware) => fn($params) => (new $middleware)->handle($params, $next),
$handler
);
return $pipeline($params);
}
/**
* 정적 파일 서빙 (이미지, CSS, JS 등)
*/
private function serveStaticFile(string $file): Response
{
return (new Response())
->header('Content-Type', mime_content_type($file))
->setContent(file_get_contents($file));
}
}