ModuleLoader.php 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. <?php
  2. declare(strict_types=1);
  3. namespace Glacier\Core;
  4. /**
  5. * 模块加载器(子空间机制,架构约定 docs/architecture.md §4)。
  6. *
  7. * 扫描 app/modules/<name>/,每个目录即一个独立子空间(/<name> 前缀 URL):
  8. * - module.php 模块声明(名称/描述/访问要求预留)
  9. * - routes.php 模块内路由(相对模式,如 '/'、'/item/:id')
  10. * - controllers/ 模块控制器(手动 require 加载,规避 PSR-4 目录大小写陷阱)
  11. * - views/ 模块模板(注册进 Twig)
  12. *
  13. * 安全:模块名白名单 [a-z0-9_-](小写),杜绝路径穿越;URL 与物理路径解耦。
  14. */
  15. final class ModuleLoader
  16. {
  17. /** @var list<array{name:string, declaration:array}> */
  18. private array $modules = [];
  19. /**
  20. * 注册所有模块路由到主 Router(在根路由表之后调用)。
  21. * @return list<array{name:string, declaration:array}> 已加载模块清单
  22. */
  23. public function load(Router $router): array
  24. {
  25. $modulesDir = APP_ROOT . '/app/modules';
  26. foreach (glob($modulesDir . '/*', GLOB_ONLYDIR) ?: [] as $dir) {
  27. $name = basename($dir);
  28. // 模块名白名单:小写字母/数字/下划线/连字符(与 URL 段一致)
  29. if (!preg_match('/^[a-z][a-z0-9_-]*$/', $name)) {
  30. Logger::warning('module skipped (invalid name)', ['name' => $name]);
  31. continue;
  32. }
  33. $declaration = $this->declaration($dir);
  34. $this->registerModule($router, $name, $dir);
  35. $this->modules[] = ['name' => $name, 'declaration' => $declaration];
  36. Logger::info('module loaded', ['name' => $name]);
  37. }
  38. return $this->modules;
  39. }
  40. private function registerModule(Router $router, string $name, string $dir): void
  41. {
  42. // 手动加载模块控制器(类文件按约定命名,见模块内 README 约定)
  43. $controllersDir = $dir . '/controllers';
  44. if (is_dir($controllersDir)) {
  45. foreach (glob($controllersDir . '/*.php') ?: [] as $file) {
  46. require_once $file;
  47. }
  48. }
  49. // 模块模板目录注册进 Twig(命名空间 = 模块名,模板用 @<name>/xxx 引用)
  50. $viewsDir = $dir . '/views';
  51. if (is_dir($viewsDir)) {
  52. View::addPath($viewsDir, $name);
  53. }
  54. // 模块内路由(相对模式)拼上 /<name> 前缀
  55. $routesFile = $dir . '/routes.php';
  56. if (!is_file($routesFile)) {
  57. return;
  58. }
  59. foreach ((array) require $routesFile as $route) {
  60. $router->add(
  61. $route['method'],
  62. '/' . $name . ($route['pattern'] === '/' ? '' : $route['pattern']),
  63. $route['handler'],
  64. $route['middleware'] ?? []
  65. );
  66. }
  67. }
  68. /** @return array 模块声明(缺失时给默认)。 */
  69. private function declaration(string $dir): array
  70. {
  71. $file = $dir . '/module.php';
  72. if (!is_file($file)) {
  73. return [];
  74. }
  75. $declaration = require $file;
  76. return is_array($declaration) ? $declaration : [];
  77. }
  78. }