index.php 4.0 KB

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