index.php 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * Glacier 单一入口(前端控制器)。
  5. * 所有动态请求都经此进入 —— URL 与物理路径完全解耦,见 docs/architecture.md §3.1。
  6. */
  7. define('APP_ROOT', dirname(__DIR__));
  8. require APP_ROOT . '/vendor/autoload.php';
  9. use Glacier\Core\Config;
  10. use Glacier\Core\Logger;
  11. use Glacier\Core\Middleware;
  12. use Glacier\Core\ModuleLoader;
  13. use Glacier\Core\Response;
  14. use Glacier\Core\Router;
  15. use Glacier\Core\Session;
  16. use Glacier\Middleware\AuthMiddleware;
  17. use Glacier\Middleware\CsrfMiddleware;
  18. use Glacier\Middleware\PermissionMiddleware;
  19. date_default_timezone_set('Asia/Shanghai'); // 架构约定 UTC+8
  20. $config = require APP_ROOT . '/app/config/env.php';
  21. Config::load($config);
  22. Logger::init(Config::get('storage.logs', APP_ROOT . '/storage/logs'));
  23. // 会话启动(安全 Cookie 参数见 Session 封装)
  24. Session::start();
  25. // 注册中间件(命名中间件,路由表按名挂载)
  26. Middleware::register('csrf', [new CsrfMiddleware(), 'handle']);
  27. Middleware::register('auth', [new AuthMiddleware(), 'handle']);
  28. Middleware::register('permission', [new PermissionMiddleware(), 'handle']);
  29. try {
  30. // 解析请求路径(去 query)
  31. $requestUri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
  32. // 部署前缀自动检测:支持子目录部署(如 http://host/glacier/xxx)。
  33. // 根部署(DocumentRoot 直指 public)时前缀为空,行为不变。
  34. $scriptName = str_replace('\\', '/', $_SERVER['SCRIPT_NAME'] ?? '/index.php');
  35. $basePath = rtrim(str_replace('\\', '/', dirname($scriptName)), '/');
  36. if ($basePath === '.' || $basePath === '/') {
  37. $basePath = '';
  38. }
  39. if ($basePath !== '' && str_starts_with($requestUri, $basePath)) {
  40. $requestUri = substr($requestUri, strlen($basePath)) ?: '/';
  41. }
  42. // 规范化尾部斜杠
  43. $path = $requestUri;
  44. if (strlen($path) > 1) {
  45. $path = rtrim($path, '/');
  46. }
  47. $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
  48. // 开发辅助:静态资源(/assets/*)直接服务。
  49. // 生产环境由 Web 服务器完成(Caddy/ Apache 的"真实文件直接返回"语义)。
  50. // 仅限 public/assets 目录内,realpath 校验防路径穿越;URL 不映射业务物理路径。
  51. if (str_starts_with($path, '/assets/')) {
  52. $assetFile = APP_ROOT . '/public' . $path;
  53. $real = realpath($assetFile);
  54. $assetRoot = realpath(APP_ROOT . '/public/assets');
  55. if ($real !== false && $assetRoot !== false && str_starts_with($real, $assetRoot)) {
  56. // 常见类型映射(Windows fileinfo 对 css 等识别不稳定)
  57. $ext = strtolower(pathinfo($real, PATHINFO_EXTENSION));
  58. $mimeMap = [
  59. 'css' => 'text/css',
  60. 'js' => 'application/javascript',
  61. 'json' => 'application/json',
  62. 'png' => 'image/png',
  63. 'jpg' => 'image/jpeg',
  64. 'jpeg' => 'image/jpeg',
  65. 'gif' => 'image/gif',
  66. 'svg' => 'image/svg+xml',
  67. 'webp' => 'image/webp',
  68. 'ico' => 'image/x-icon',
  69. 'woff' => 'font/woff',
  70. 'woff2' => 'font/woff2',
  71. ];
  72. $mime = $mimeMap[$ext] ?? (function_exists('mime_content_type') ? mime_content_type($real) : false) ?: 'application/octet-stream';
  73. header('Content-Type: ' . $mime);
  74. readfile($real);
  75. exit;
  76. }
  77. }
  78. Logger::info('request', ['method' => $method, 'path' => $path]);
  79. // 注册路由:先根路由表,再挂载业务模块(子空间 /<name>)
  80. $router = new Router();
  81. foreach ((array) require APP_ROOT . '/app/config/routes.php' as $route) {
  82. $router->add($route['method'], $route['pattern'], $route['handler'], $route['middleware'] ?? []);
  83. }
  84. (new ModuleLoader())->load($router);
  85. $match = $router->match($path, $method);
  86. if ($match === null) {
  87. Logger::warning('route not found', ['method' => $method, 'path' => $path]);
  88. Response::html(
  89. '<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>404</title>'
  90. . '<style>body{font-family:system-ui;text-align:center;padding-top:80px;color:#666}'
  91. . 'h1{font-size:48px;color:#c33;margin-bottom:8px}</style></head>'
  92. . '<body><h1>404</h1><p>页面不存在。</p></body></html>',
  93. 404
  94. );
  95. }
  96. // 执行路由中间件(拒绝时中间件自行终止响应)
  97. Middleware::run($match['middleware'], $match['params']);
  98. call_user_func($match['handler'], $match['params']);
  99. } catch (\Throwable $e) {
  100. Logger::error('uncaught exception', [
  101. 'message' => $e->getMessage(),
  102. 'file' => $e->getFile(),
  103. 'line' => $e->getLine(),
  104. ]);
  105. $detail = Config::get('app.debug', false)
  106. ? '<pre>' . e($e->getMessage()) . PHP_EOL . e($e->getFile() . ':' . $e->getLine()) . '</pre>'
  107. : '<p>服务器开小差了,请稍后再试。</p>';
  108. Response::html(
  109. '<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>500</title>'
  110. . '<style>body{font-family:system-ui;text-align:center;padding-top:80px;color:#666}'
  111. . 'h1{font-size:48px;color:#c33}</style></head>'
  112. . '<body><h1>500</h1>' . $detail . '</body></html>',
  113. 500
  114. );
  115. }