Skip to main content

sz_rust_addons_loader/
loader.rs

1//! 插件加载器(主入口)
2//!
3//! ## PHP 对齐
4//!
5//! 对齐 PHP `think\addons\Service` 的完整生命周期:
6//!
7//! ```php
8//! // vendor/zzstudio/think-addons/src/addons/Service.php
9//! class Service extends \think\Service
10//! {
11//!     public function register()
12//!     {
13//!         $this->addons_path = $this->getAddonsPath();
14//!         Lang::load([...]);
15//!         $this->autoload();    // ← 自动扫描插件目录
16//!         $this->loadEvent();   // ← 加载钩子
17//!         $this->loadService(); // ← 加载 service.ini
18//!         $this->app->bind('addons', Service::class);
19//!     }
20//!
21//!     public function boot()
22//!     {
23//!         // 注册路由:addons/:addon/[:controller]/[:action]
24//!         $route->rule("addons/:addon/[:controller]/[:action]", $execute);
25//!     }
26//! }
27//! ```
28//!
29//! ## 模块结构
30//!
31//! | 模块 | 对齐 PHP | 子任务 |
32//! |------|---------|--------|
33//! | `loader` | `Service::register/autoload/loadEvent/loadService` | 10.1 |
34//! | `registry` | 插件状态管理(隐式) | 10.2 |
35//! | `manifest` | `Addons::getInfo()` | 10.1 |
36//! | `autoload` | `spl_autoload_register` | 10.1 |
37//! | `route` | `Route::execute` | 10.3 |
38
39use std::path::{Path, PathBuf};
40use std::sync::Arc;
41
42use crate::autoload::AddonAutoload;
43use crate::error::{AddonLoaderError, AddonLoaderResult};
44use crate::manifest::AddonManifest;
45use crate::registry::AddonRegistry;
46
47/// 插件加载器(主入口)
48///
49/// 对齐 PHP `think\addons\Service`,统一管理插件发现/清单/注册/路由解析。
50///
51/// ## 设计
52///
53/// - 持有插件根目录(对齐 PHP `$this->addons_path`)
54/// - 持有 `AddonRegistry`(插件清单注册中心)
55/// - 持有 `AddonAutoload`(类名→文件路径解析)
56/// - 提供 `register()` 入口:扫描目录 + 解析清单 + 注册到 registry
57/// - 提供 `parse_route(url)` 入口:URL → 控制器类名 + 文件路径
58///
59/// ## 用法
60///
61/// ```rust,ignore
62/// use sz_rust_addons_loader::loader::AddonLoader;
63///
64/// let loader = AddonLoader::new("/path/to/addons");
65/// let errors = loader.register().unwrap();
66/// // errors 中包含解析失败的插件错误(不中断整体扫描)
67///
68/// let route = loader.parse_route("/addons/operate/Order/index").unwrap();
69/// assert_eq!(route.addon, "operate");
70/// assert_eq!(route.controller, "Order");
71/// assert_eq!(route.action, "index");
72/// ```
73#[derive(Debug)]
74pub struct AddonLoader {
75    /// 插件根目录(对齐 PHP `getAddonsPath()`)
76    addons_path: PathBuf,
77    /// 插件注册中心(线程安全)
78    registry: Arc<AddonRegistry>,
79    /// 自动加载器
80    autoload: AddonAutoload,
81}
82
83impl AddonLoader {
84    /// 创建加载器
85    ///
86    /// - `addons_path`:插件根目录,对齐 PHP `{rootPath}/addons/`
87    pub fn new(addons_path: impl Into<PathBuf>) -> Self {
88        let addons_path = addons_path.into();
89        let autoload = AddonAutoload::new(&addons_path);
90        Self {
91            addons_path,
92            registry: Arc::new(AddonRegistry::new()),
93            autoload,
94        }
95    }
96
97    /// 获取插件根目录
98    pub fn addons_path(&self) -> &Path {
99        &self.addons_path
100    }
101
102    /// 获取注册中心引用(共享只读)
103    pub fn registry(&self) -> &AddonRegistry {
104        &self.registry
105    }
106
107    /// 获取自动加载器引用
108    pub fn autoload(&self) -> &AddonAutoload {
109        &self.autoload
110    }
111
112    /// 注册所有插件(对齐 PHP `Service::register()` 完整流程)
113    ///
114    /// ## 流程
115    ///
116    /// 1. 扫描 `addons_path` 下所有子目录(对齐 PHP `scandir`)
117    /// 2. 对每个子目录解析 `Plugin.php` 清单(对齐 PHP `autoload()`)
118    /// 3. 注册到 `registry`(对齐 PHP `get_addons_instance` 缓存)
119    /// 4. 不加载钩子和服务绑定(Rust 侧无 PHP 反射能力,钩子由调用方显式注册)
120    ///
121    /// ## 错误处理
122    ///
123    /// - 单个插件解析失败不会中断整体扫描
124    /// - 返回的 `Vec<AddonLoaderError>` 包含所有失败的插件错误
125    /// - 目录读取失败返回 `ScanDir` 错误
126    #[tracing::instrument(skip(self))]
127    pub fn register(&self) -> AddonLoaderResult<Vec<AddonLoaderError>> {
128        self.registry.load_from_directory(&self.addons_path)
129    }
130
131    /// 解析路由(对齐 PHP `Route::execute($addon, $controller, $action)`)
132    ///
133    /// ## 错误
134    ///
135    /// - `RouteParse`:URL 格式错误
136    /// - `AddonNotFound`:插件不存在
137    /// - `AddonDisabled`:插件已禁用
138    /// - `ControllerNotFound`:控制器不存在
139    #[tracing::instrument(skip(self))]
140    pub fn parse_route(&self, url: &str) -> AddonLoaderResult<crate::route::AddonRoute> {
141        crate::route::parse_route(url, &self.registry, &self.autoload)
142    }
143
144    /// 获取所有已注册插件名(按字母序)
145    pub fn names(&self) -> Vec<String> {
146        self.registry.names()
147    }
148
149    /// 获取所有已注册插件清单(按字母序)
150    pub fn all_addons(&self) -> Vec<AddonManifest> {
151        self.registry.all()
152    }
153
154    /// 获取所有启用的插件清单(按字母序)
155    pub fn enabled_addons(&self) -> Vec<AddonManifest> {
156        self.registry.enabled_addons()
157    }
158
159    /// 获取所有禁用的插件清单(按字母序)
160    pub fn disabled_addons(&self) -> Vec<AddonManifest> {
161        self.registry.disabled_addons()
162    }
163
164    /// 判断插件是否存在
165    pub fn exists(&self, name: &str) -> bool {
166        self.registry.exists(name)
167    }
168
169    /// 判断插件是否启用
170    pub fn is_enabled(&self, name: &str) -> AddonLoaderResult<bool> {
171        self.registry.is_enabled(name)
172    }
173
174    /// 获取插件清单
175    pub fn get_manifest(&self, name: &str) -> AddonLoaderResult<AddonManifest> {
176        self.registry.get(name)
177    }
178
179    /// 获取已注册插件数量
180    pub fn count(&self) -> usize {
181        self.registry.count()
182    }
183
184    /// 解析类名到文件路径(对齐 PHP `spl_autoload_register` 回调)
185    pub fn resolve_class(&self, class: &str) -> AddonLoaderResult<Option<PathBuf>> {
186        self.autoload.resolve(class)
187    }
188
189    /// 解析控制器类名到文件路径(对齐 PHP `get_addons_class` + `class_exists`)
190    pub fn resolve_controller(
191        &self,
192        addon: &str,
193        controller: &str,
194    ) -> AddonLoaderResult<Option<PathBuf>> {
195        self.autoload.resolve_controller(addon, controller)
196    }
197
198    /// 解析插件入口类到文件路径
199    pub fn resolve_plugin(&self, addon: &str) -> AddonLoaderResult<Option<PathBuf>> {
200        self.autoload.resolve_plugin(addon)
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use std::fs;
208
209    fn make_test_env() -> (tempfile::TempDir, AddonLoader) {
210        let tmp = tempfile::tempdir().expect("create tempdir");
211        let addons_path = tmp.path().join("addons");
212
213        // operate 插件
214        let operate_dir = addons_path.join("operate");
215        fs::create_dir_all(&operate_dir).unwrap();
216        fs::write(
217            operate_dir.join("Plugin.php"),
218            r#"
219public $info = [
220    'name' => 'operate',
221    'title' => '运营管理',
222    'status' => 1,
223];
224"#,
225        )
226        .unwrap();
227
228        // operate/controller/Order.php
229        let controller_dir = operate_dir.join("controller");
230        fs::create_dir_all(&controller_dir).unwrap();
231        fs::write(controller_dir.join("Order.php"), "<?php // stub").unwrap();
232
233        // operate/controller/admin/Order.php
234        let admin_dir = controller_dir.join("admin");
235        fs::create_dir_all(&admin_dir).unwrap();
236        fs::write(admin_dir.join("Order.php"), "<?php // stub").unwrap();
237
238        // cashier 插件(status=0 禁用)
239        let cashier_dir = addons_path.join("cashier");
240        fs::create_dir_all(&cashier_dir).unwrap();
241        fs::write(
242            cashier_dir.join("Plugin.php"),
243            r#"
244public $info = [
245    'name' => 'cashier',
246    'status' => 0,
247];
248"#,
249        )
250        .unwrap();
251
252        let loader = AddonLoader::new(&addons_path);
253        (tmp, loader)
254    }
255
256    #[test]
257    fn test_new_loader() {
258        let loader = AddonLoader::new("/addons");
259        assert_eq!(loader.addons_path(), Path::new("/addons"));
260        assert_eq!(loader.count(), 0);
261    }
262
263    #[test]
264    fn test_register_scans_addons() {
265        let (_tmp, loader) = make_test_env();
266        let errors = loader.register().unwrap();
267        assert!(errors.is_empty(), "errors: {:?}", errors);
268        assert_eq!(loader.count(), 2);
269        assert!(loader.exists("operate"));
270        assert!(loader.exists("cashier"));
271    }
272
273    #[test]
274    fn test_register_returns_errors_for_malformed() {
275        let tmp = tempfile::tempdir().unwrap();
276        let addons_path = tmp.path().join("addons");
277        let malformed_dir = addons_path.join("malformed");
278        fs::create_dir_all(&malformed_dir).unwrap();
279        // 写入无 $info 的 Plugin.php
280        fs::write(malformed_dir.join("Plugin.php"), "<?php class Plugin {}").unwrap();
281
282        let loader = AddonLoader::new(&addons_path);
283        let errors = loader.register().unwrap();
284        assert!(!errors.is_empty());
285        assert_eq!(loader.count(), 0);
286    }
287
288    #[test]
289    fn test_register_nonexistent_path() {
290        let loader = AddonLoader::new("/nonexistent/path/12345");
291        let result = loader.register();
292        assert!(result.is_err());
293    }
294
295    #[test]
296    fn test_parse_route_valid() {
297        let (_tmp, loader) = make_test_env();
298        loader.register().unwrap();
299
300        let route = loader.parse_route("/addons/operate/Order/index").unwrap();
301        assert_eq!(route.addon, "operate");
302        assert_eq!(route.controller, "Order");
303        assert_eq!(route.action, "index");
304        assert!(route.controller_file.is_some());
305    }
306
307    #[test]
308    fn test_parse_route_dotted_controller() {
309        let (_tmp, loader) = make_test_env();
310        loader.register().unwrap();
311
312        let route = loader
313            .parse_route("/addons/operate/admin.Order/index")
314            .unwrap();
315        assert_eq!(route.controller, "admin.Order");
316        assert!(route.controller_file.is_some());
317    }
318
319    #[test]
320    fn test_parse_route_disabled_addon() {
321        let (_tmp, loader) = make_test_env();
322        loader.register().unwrap();
323
324        let result = loader.parse_route("/addons/cashier/Order/index");
325        assert!(result.is_err());
326        match result.unwrap_err() {
327            AddonLoaderError::AddonDisabled(name) => assert_eq!(name, "cashier"),
328            other => panic!("expected AddonDisabled, got {:?}", other),
329        }
330    }
331
332    #[test]
333    fn test_parse_route_not_found_addon() {
334        let (_tmp, loader) = make_test_env();
335        loader.register().unwrap();
336
337        let result = loader.parse_route("/addons/ghost/Order/index");
338        assert!(result.is_err());
339    }
340
341    #[test]
342    fn test_parse_route_controller_not_found() {
343        let (_tmp, loader) = make_test_env();
344        loader.register().unwrap();
345
346        let result = loader.parse_route("/addons/operate/Ghost/index");
347        assert!(result.is_err());
348    }
349
350    #[test]
351    fn test_names_sorted() {
352        let (_tmp, loader) = make_test_env();
353        loader.register().unwrap();
354
355        let names = loader.names();
356        assert_eq!(names, vec!["cashier", "operate"]);
357    }
358
359    #[test]
360    fn test_all_addons_sorted() {
361        let (_tmp, loader) = make_test_env();
362        loader.register().unwrap();
363
364        let all = loader.all_addons();
365        assert_eq!(all.len(), 2);
366        assert_eq!(all[0].name, "cashier");
367        assert_eq!(all[1].name, "operate");
368    }
369
370    #[test]
371    fn test_enabled_addons_filtered() {
372        let (_tmp, loader) = make_test_env();
373        loader.register().unwrap();
374
375        let enabled = loader.enabled_addons();
376        assert_eq!(enabled.len(), 1);
377        assert_eq!(enabled[0].name, "operate");
378    }
379
380    #[test]
381    fn test_disabled_addons_filtered() {
382        let (_tmp, loader) = make_test_env();
383        loader.register().unwrap();
384
385        let disabled = loader.disabled_addons();
386        assert_eq!(disabled.len(), 1);
387        assert_eq!(disabled[0].name, "cashier");
388    }
389
390    #[test]
391    fn test_exists() {
392        let (_tmp, loader) = make_test_env();
393        loader.register().unwrap();
394
395        assert!(loader.exists("operate"));
396        assert!(loader.exists("cashier"));
397        assert!(!loader.exists("ghost"));
398    }
399
400    #[test]
401    fn test_is_enabled() {
402        let (_tmp, loader) = make_test_env();
403        loader.register().unwrap();
404
405        assert!(loader.is_enabled("operate").unwrap());
406        assert!(!loader.is_enabled("cashier").unwrap());
407        assert!(loader.is_enabled("ghost").is_err());
408    }
409
410    #[test]
411    fn test_get_manifest() {
412        let (_tmp, loader) = make_test_env();
413        loader.register().unwrap();
414
415        let manifest = loader.get_manifest("operate").unwrap();
416        assert_eq!(manifest.name, "operate");
417        assert_eq!(manifest.title, "运营管理");
418    }
419
420    #[test]
421    fn test_get_manifest_not_found() {
422        let (_tmp, loader) = make_test_env();
423        loader.register().unwrap();
424
425        let result = loader.get_manifest("ghost");
426        assert!(result.is_err());
427    }
428
429    #[test]
430    fn test_count() {
431        let (_tmp, loader) = make_test_env();
432        assert_eq!(loader.count(), 0);
433        loader.register().unwrap();
434        assert_eq!(loader.count(), 2);
435    }
436
437    #[test]
438    fn test_resolve_class() {
439        let (_tmp, loader) = make_test_env();
440        loader.register().unwrap();
441
442        let path = loader.resolve_class("addons\\operate\\Plugin").unwrap();
443        assert!(path.is_some());
444    }
445
446    #[test]
447    fn test_resolve_class_non_addons() {
448        let (_tmp, loader) = make_test_env();
449        loader.register().unwrap();
450
451        let path = loader.resolve_class("app\\Home").unwrap();
452        assert!(path.is_none());
453    }
454
455    #[test]
456    fn test_resolve_controller() {
457        let (_tmp, loader) = make_test_env();
458        loader.register().unwrap();
459
460        let path = loader.resolve_controller("operate", "Order").unwrap();
461        assert!(path.is_some());
462    }
463
464    #[test]
465    fn test_resolve_controller_multilevel() {
466        let (_tmp, loader) = make_test_env();
467        loader.register().unwrap();
468
469        let path = loader.resolve_controller("operate", "admin.Order").unwrap();
470        assert!(path.is_some());
471    }
472
473    #[test]
474    fn test_resolve_plugin() {
475        let (_tmp, loader) = make_test_env();
476        loader.register().unwrap();
477
478        let path = loader.resolve_plugin("operate").unwrap();
479        assert!(path.is_some());
480    }
481
482    #[test]
483    fn test_registry_shared_via_arc() {
484        let (_tmp, loader) = make_test_env();
485        loader.register().unwrap();
486
487        // 通过 registry() 访问的 Arc 应该指向同一份数据
488        let registry = loader.registry();
489        assert_eq!(registry.count(), 2);
490        assert!(registry.exists("operate"));
491    }
492
493    #[test]
494    fn test_autoload_accessor() {
495        let (_tmp, loader) = make_test_env();
496        loader.register().unwrap();
497
498        let autoload = loader.autoload();
499        assert_eq!(autoload.addons_path(), loader.addons_path());
500    }
501}