|
|
@@ -0,0 +1,84 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+declare(strict_types=1);
|
|
|
+
|
|
|
+namespace Glacier\Core;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 模块加载器(子空间机制,架构约定 docs/architecture.md §4)。
|
|
|
+ *
|
|
|
+ * 扫描 app/modules/<name>/,每个目录即一个独立子空间(/<name> 前缀 URL):
|
|
|
+ * - module.php 模块声明(名称/描述/访问要求预留)
|
|
|
+ * - routes.php 模块内路由(相对模式,如 '/'、'/item/:id')
|
|
|
+ * - controllers/ 模块控制器(手动 require 加载,规避 PSR-4 目录大小写陷阱)
|
|
|
+ * - views/ 模块模板(注册进 Twig)
|
|
|
+ *
|
|
|
+ * 安全:模块名白名单 [a-z0-9_-](小写),杜绝路径穿越;URL 与物理路径解耦。
|
|
|
+ */
|
|
|
+final class ModuleLoader
|
|
|
+{
|
|
|
+ /** @var list<array{name:string, declaration:array}> */
|
|
|
+ private array $modules = [];
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 注册所有模块路由到主 Router(在根路由表之后调用)。
|
|
|
+ * @return list<array{name:string, declaration:array}> 已加载模块清单
|
|
|
+ */
|
|
|
+ public function load(Router $router): array
|
|
|
+ {
|
|
|
+ $modulesDir = APP_ROOT . '/app/modules';
|
|
|
+ foreach (glob($modulesDir . '/*', GLOB_ONLYDIR) ?: [] as $dir) {
|
|
|
+ $name = basename($dir);
|
|
|
+ // 模块名白名单:小写字母/数字/下划线/连字符(与 URL 段一致)
|
|
|
+ if (!preg_match('/^[a-z][a-z0-9_-]*$/', $name)) {
|
|
|
+ Logger::warning('module skipped (invalid name)', ['name' => $name]);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ $declaration = $this->declaration($dir);
|
|
|
+ $this->registerModule($router, $name, $dir);
|
|
|
+ $this->modules[] = ['name' => $name, 'declaration' => $declaration];
|
|
|
+ Logger::info('module loaded', ['name' => $name]);
|
|
|
+ }
|
|
|
+ return $this->modules;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function registerModule(Router $router, string $name, string $dir): void
|
|
|
+ {
|
|
|
+ // 手动加载模块控制器(类文件按约定命名,见模块内 README 约定)
|
|
|
+ $controllersDir = $dir . '/controllers';
|
|
|
+ if (is_dir($controllersDir)) {
|
|
|
+ foreach (glob($controllersDir . '/*.php') ?: [] as $file) {
|
|
|
+ require_once $file;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 模块模板目录注册进 Twig(命名空间 = 模块名,模板用 @<name>/xxx 引用)
|
|
|
+ $viewsDir = $dir . '/views';
|
|
|
+ if (is_dir($viewsDir)) {
|
|
|
+ View::addPath($viewsDir, $name);
|
|
|
+ }
|
|
|
+ // 模块内路由(相对模式)拼上 /<name> 前缀
|
|
|
+ $routesFile = $dir . '/routes.php';
|
|
|
+ if (!is_file($routesFile)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ foreach ((array) require $routesFile as $route) {
|
|
|
+ $router->add(
|
|
|
+ $route['method'],
|
|
|
+ '/' . $name . ($route['pattern'] === '/' ? '' : $route['pattern']),
|
|
|
+ $route['handler'],
|
|
|
+ $route['middleware'] ?? []
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** @return array 模块声明(缺失时给默认)。 */
|
|
|
+ private function declaration(string $dir): array
|
|
|
+ {
|
|
|
+ $file = $dir . '/module.php';
|
|
|
+ if (!is_file($file)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ $declaration = require $file;
|
|
|
+ return is_array($declaration) ? $declaration : [];
|
|
|
+ }
|
|
|
+}
|