index.php 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  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\ModuleLoader;
  12. use Glacier\Core\Response;
  13. use Glacier\Core\Router;
  14. date_default_timezone_set('Asia/Shanghai'); // 架构约定 UTC+8
  15. $config = require APP_ROOT . '/app/config/env.php';
  16. Config::load($config);
  17. Logger::init(Config::get('storage.logs', APP_ROOT . '/storage/logs'));
  18. try {
  19. // 解析请求路径(去 query,规范化尾部斜杠)
  20. $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
  21. if (strlen($path) > 1) {
  22. $path = rtrim($path, '/');
  23. }
  24. $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
  25. // 开发辅助:静态资源(/assets/*)直接服务。
  26. // 生产环境由 Web 服务器完成(Caddy/ Apache 的"真实文件直接返回"语义)。
  27. // 仅限 public/assets 目录内,realpath 校验防路径穿越;URL 不映射业务物理路径。
  28. if (str_starts_with($path, '/assets/')) {
  29. $assetFile = APP_ROOT . '/public' . $path;
  30. $real = realpath($assetFile);
  31. $assetRoot = realpath(APP_ROOT . '/public/assets');
  32. if ($real !== false && $assetRoot !== false && str_starts_with($real, $assetRoot)) {
  33. // 常见类型映射(Windows fileinfo 对 css 等识别不稳定)
  34. $ext = strtolower(pathinfo($real, PATHINFO_EXTENSION));
  35. $mimeMap = [
  36. 'css' => 'text/css',
  37. 'js' => 'application/javascript',
  38. 'json' => 'application/json',
  39. 'png' => 'image/png',
  40. 'jpg' => 'image/jpeg',
  41. 'jpeg' => 'image/jpeg',
  42. 'gif' => 'image/gif',
  43. 'svg' => 'image/svg+xml',
  44. 'webp' => 'image/webp',
  45. 'ico' => 'image/x-icon',
  46. 'woff' => 'font/woff',
  47. 'woff2' => 'font/woff2',
  48. ];
  49. $mime = $mimeMap[$ext] ?? (function_exists('mime_content_type') ? mime_content_type($real) : false) ?: 'application/octet-stream';
  50. header('Content-Type: ' . $mime);
  51. readfile($real);
  52. exit;
  53. }
  54. }
  55. Logger::info('request', ['method' => $method, 'path' => $path]);
  56. // 注册路由:先根路由表,再挂载业务模块(子空间 /<name>)
  57. $router = new Router();
  58. foreach ((array) require APP_ROOT . '/app/config/routes.php' as $route) {
  59. $router->add($route['method'], $route['pattern'], $route['handler'], $route['middleware'] ?? []);
  60. }
  61. (new ModuleLoader())->load($router);
  62. $match = $router->match($path, $method);
  63. if ($match === null) {
  64. Logger::warning('route not found', ['method' => $method, 'path' => $path]);
  65. Response::html(
  66. '<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>404</title>'
  67. . '<style>body{font-family:system-ui;text-align:center;padding-top:80px;color:#666}'
  68. . 'h1{font-size:48px;color:#c33;margin-bottom:8px}</style></head>'
  69. . '<body><h1>404</h1><p>页面不存在。</p></body></html>',
  70. 404
  71. );
  72. }
  73. call_user_func($match['handler'], $match['params']);
  74. } catch (\Throwable $e) {
  75. Logger::error('uncaught exception', [
  76. 'message' => $e->getMessage(),
  77. 'file' => $e->getFile(),
  78. 'line' => $e->getLine(),
  79. ]);
  80. $detail = Config::get('app.debug', false)
  81. ? '<pre>' . e($e->getMessage()) . PHP_EOL . e($e->getFile() . ':' . $e->getLine()) . '</pre>'
  82. : '<p>服务器开小差了,请稍后再试。</p>';
  83. Response::html(
  84. '<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><title>500</title>'
  85. . '<style>body{font-family:system-ui;text-align:center;padding-top:80px;color:#666}'
  86. . 'h1{font-size:48px;color:#c33}</style></head>'
  87. . '<body><h1>500</h1>' . $detail . '</body></html>',
  88. 500
  89. );
  90. }