Skip to main content

sz_rust_addons_loader/
loader.rs

1//! 插件加载器(Phase 10.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/// 插件加载器(Phase 10.1 主入口)
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    pub fn register(&self) -> AddonLoaderResult<Vec<AddonLoaderError>> {
127        self.registry.load_from_directory(&self.addons_path)
128    }
129
130    /// 解析路由(对齐 PHP `Route::execute($addon, $controller, $action)`)
131    ///
132    /// ## 错误
133    ///
134    /// - `RouteParse`:URL 格式错误
135    /// - `AddonNotFound`:插件不存在
136    /// - `AddonDisabled`:插件已禁用
137    /// - `ControllerNotFound`:控制器不存在
138    pub fn parse_route(&self, url: &str) -> AddonLoaderResult<crate::route::AddonRoute> {
139        crate::route::parse_route(url, &self.registry, &self.autoload)
140    }
141
142    /// 获取所有已注册插件名(按字母序)
143    pub fn names(&self) -> Vec<String> {
144        self.registry.names()
145    }
146
147    /// 获取所有已注册插件清单(按字母序)
148    pub fn all_addons(&self) -> Vec<AddonManifest> {
149        self.registry.all()
150    }
151
152    /// 获取所有启用的插件清单(按字母序)
153    pub fn enabled_addons(&self) -> Vec<AddonManifest> {
154        self.registry.enabled_addons()
155    }
156
157    /// 获取所有禁用的插件清单(按字母序)
158    pub fn disabled_addons(&self) -> Vec<AddonManifest> {
159        self.registry.disabled_addons()
160    }
161
162    /// 判断插件是否存在
163    pub fn exists(&self, name: &str) -> bool {
164        self.registry.exists(name)
165    }
166
167    /// 判断插件是否启用
168    pub fn is_enabled(&self, name: &str) -> AddonLoaderResult<bool> {
169        self.registry.is_enabled(name)
170    }
171
172    /// 获取插件清单
173    pub fn get_manifest(&self, name: &str) -> AddonLoaderResult<AddonManifest> {
174        self.registry.get(name)
175    }
176
177    /// 获取已注册插件数量
178    pub fn count(&self) -> usize {
179        self.registry.count()
180    }
181
182    /// 解析类名到文件路径(对齐 PHP `spl_autoload_register` 回调)
183    pub fn resolve_class(&self, class: &str) -> AddonLoaderResult<Option<PathBuf>> {
184        self.autoload.resolve(class)
185    }
186
187    /// 解析控制器类名到文件路径(对齐 PHP `get_addons_class` + `class_exists`)
188    pub fn resolve_controller(
189        &self,
190        addon: &str,
191        controller: &str,
192    ) -> AddonLoaderResult<Option<PathBuf>> {
193        self.autoload.resolve_controller(addon, controller)
194    }
195
196    /// 解析插件入口类到文件路径
197    pub fn resolve_plugin(&self, addon: &str) -> AddonLoaderResult<Option<PathBuf>> {
198        self.autoload.resolve_plugin(addon)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use std::fs;
206
207    fn make_test_env() -> (tempfile::TempDir, AddonLoader) {
208        let tmp = tempfile::tempdir().expect("create tempdir");
209        let addons_path = tmp.path().join("addons");
210
211        // operate 插件
212        let operate_dir = addons_path.join("operate");
213        fs::create_dir_all(&operate_dir).unwrap();
214        fs::write(
215            operate_dir.join("Plugin.php"),
216            r#"
217public $info = [
218    'name' => 'operate',
219    'title' => '运营管理',
220    'status' => 1,
221];
222"#,
223        )
224        .unwrap();
225
226        // operate/controller/Order.php
227        let controller_dir = operate_dir.join("controller");
228        fs::create_dir_all(&controller_dir).unwrap();
229        fs::write(controller_dir.join("Order.php"), "<?php // stub").unwrap();
230
231        // operate/controller/admin/Order.php
232        let admin_dir = controller_dir.join("admin");
233        fs::create_dir_all(&admin_dir).unwrap();
234        fs::write(admin_dir.join("Order.php"), "<?php // stub").unwrap();
235
236        // cashier 插件(status=0 禁用)
237        let cashier_dir = addons_path.join("cashier");
238        fs::create_dir_all(&cashier_dir).unwrap();
239        fs::write(
240            cashier_dir.join("Plugin.php"),
241            r#"
242public $info = [
243    'name' => 'cashier',
244    'status' => 0,
245];
246"#,
247        )
248        .unwrap();
249
250        let loader = AddonLoader::new(&addons_path);
251        (tmp, loader)
252    }
253
254    #[test]
255    fn test_new_loader() {
256        let loader = AddonLoader::new("/addons");
257        assert_eq!(loader.addons_path(), Path::new("/addons"));
258        assert_eq!(loader.count(), 0);
259    }
260
261    #[test]
262    fn test_register_scans_addons() {
263        let (_tmp, loader) = make_test_env();
264        let errors = loader.register().unwrap();
265        assert!(errors.is_empty(), "errors: {:?}", errors);
266        assert_eq!(loader.count(), 2);
267        assert!(loader.exists("operate"));
268        assert!(loader.exists("cashier"));
269    }
270
271    #[test]
272    fn test_register_returns_errors_for_malformed() {
273        let tmp = tempfile::tempdir().unwrap();
274        let addons_path = tmp.path().join("addons");
275        let malformed_dir = addons_path.join("malformed");
276        fs::create_dir_all(&malformed_dir).unwrap();
277        // 写入无 $info 的 Plugin.php
278        fs::write(malformed_dir.join("Plugin.php"), "<?php class Plugin {}").unwrap();
279
280        let loader = AddonLoader::new(&addons_path);
281        let errors = loader.register().unwrap();
282        assert!(!errors.is_empty());
283        assert_eq!(loader.count(), 0);
284    }
285
286    #[test]
287    fn test_register_nonexistent_path() {
288        let loader = AddonLoader::new("/nonexistent/path/12345");
289        let result = loader.register();
290        assert!(result.is_err());
291    }
292
293    #[test]
294    fn test_parse_route_valid() {
295        let (_tmp, loader) = make_test_env();
296        loader.register().unwrap();
297
298        let route = loader.parse_route("/addons/operate/Order/index").unwrap();
299        assert_eq!(route.addon, "operate");
300        assert_eq!(route.controller, "Order");
301        assert_eq!(route.action, "index");
302        assert!(route.controller_file.is_some());
303    }
304
305    #[test]
306    fn test_parse_route_dotted_controller() {
307        let (_tmp, loader) = make_test_env();
308        loader.register().unwrap();
309
310        let route = loader
311            .parse_route("/addons/operate/admin.Order/index")
312            .unwrap();
313        assert_eq!(route.controller, "admin.Order");
314        assert!(route.controller_file.is_some());
315    }
316
317    #[test]
318    fn test_parse_route_disabled_addon() {
319        let (_tmp, loader) = make_test_env();
320        loader.register().unwrap();
321
322        let result = loader.parse_route("/addons/cashier/Order/index");
323        assert!(result.is_err());
324        match result.unwrap_err() {
325            AddonLoaderError::AddonDisabled(name) => assert_eq!(name, "cashier"),
326            other => panic!("expected AddonDisabled, got {:?}", other),
327        }
328    }
329
330    #[test]
331    fn test_parse_route_not_found_addon() {
332        let (_tmp, loader) = make_test_env();
333        loader.register().unwrap();
334
335        let result = loader.parse_route("/addons/ghost/Order/index");
336        assert!(result.is_err());
337    }
338
339    #[test]
340    fn test_parse_route_controller_not_found() {
341        let (_tmp, loader) = make_test_env();
342        loader.register().unwrap();
343
344        let result = loader.parse_route("/addons/operate/Ghost/index");
345        assert!(result.is_err());
346    }
347
348    #[test]
349    fn test_names_sorted() {
350        let (_tmp, loader) = make_test_env();
351        loader.register().unwrap();
352
353        let names = loader.names();
354        assert_eq!(names, vec!["cashier", "operate"]);
355    }
356
357    #[test]
358    fn test_all_addons_sorted() {
359        let (_tmp, loader) = make_test_env();
360        loader.register().unwrap();
361
362        let all = loader.all_addons();
363        assert_eq!(all.len(), 2);
364        assert_eq!(all[0].name, "cashier");
365        assert_eq!(all[1].name, "operate");
366    }
367
368    #[test]
369    fn test_enabled_addons_filtered() {
370        let (_tmp, loader) = make_test_env();
371        loader.register().unwrap();
372
373        let enabled = loader.enabled_addons();
374        assert_eq!(enabled.len(), 1);
375        assert_eq!(enabled[0].name, "operate");
376    }
377
378    #[test]
379    fn test_disabled_addons_filtered() {
380        let (_tmp, loader) = make_test_env();
381        loader.register().unwrap();
382
383        let disabled = loader.disabled_addons();
384        assert_eq!(disabled.len(), 1);
385        assert_eq!(disabled[0].name, "cashier");
386    }
387
388    #[test]
389    fn test_exists() {
390        let (_tmp, loader) = make_test_env();
391        loader.register().unwrap();
392
393        assert!(loader.exists("operate"));
394        assert!(loader.exists("cashier"));
395        assert!(!loader.exists("ghost"));
396    }
397
398    #[test]
399    fn test_is_enabled() {
400        let (_tmp, loader) = make_test_env();
401        loader.register().unwrap();
402
403        assert!(loader.is_enabled("operate").unwrap());
404        assert!(!loader.is_enabled("cashier").unwrap());
405        assert!(loader.is_enabled("ghost").is_err());
406    }
407
408    #[test]
409    fn test_get_manifest() {
410        let (_tmp, loader) = make_test_env();
411        loader.register().unwrap();
412
413        let manifest = loader.get_manifest("operate").unwrap();
414        assert_eq!(manifest.name, "operate");
415        assert_eq!(manifest.title, "运营管理");
416    }
417
418    #[test]
419    fn test_get_manifest_not_found() {
420        let (_tmp, loader) = make_test_env();
421        loader.register().unwrap();
422
423        let result = loader.get_manifest("ghost");
424        assert!(result.is_err());
425    }
426
427    #[test]
428    fn test_count() {
429        let (_tmp, loader) = make_test_env();
430        assert_eq!(loader.count(), 0);
431        loader.register().unwrap();
432        assert_eq!(loader.count(), 2);
433    }
434
435    #[test]
436    fn test_resolve_class() {
437        let (_tmp, loader) = make_test_env();
438        loader.register().unwrap();
439
440        let path = loader.resolve_class("addons\\operate\\Plugin").unwrap();
441        assert!(path.is_some());
442    }
443
444    #[test]
445    fn test_resolve_class_non_addons() {
446        let (_tmp, loader) = make_test_env();
447        loader.register().unwrap();
448
449        let path = loader.resolve_class("app\\Home").unwrap();
450        assert!(path.is_none());
451    }
452
453    #[test]
454    fn test_resolve_controller() {
455        let (_tmp, loader) = make_test_env();
456        loader.register().unwrap();
457
458        let path = loader.resolve_controller("operate", "Order").unwrap();
459        assert!(path.is_some());
460    }
461
462    #[test]
463    fn test_resolve_controller_multilevel() {
464        let (_tmp, loader) = make_test_env();
465        loader.register().unwrap();
466
467        let path = loader.resolve_controller("operate", "admin.Order").unwrap();
468        assert!(path.is_some());
469    }
470
471    #[test]
472    fn test_resolve_plugin() {
473        let (_tmp, loader) = make_test_env();
474        loader.register().unwrap();
475
476        let path = loader.resolve_plugin("operate").unwrap();
477        assert!(path.is_some());
478    }
479
480    #[test]
481    fn test_registry_shared_via_arc() {
482        let (_tmp, loader) = make_test_env();
483        loader.register().unwrap();
484
485        // 通过 registry() 访问的 Arc 应该指向同一份数据
486        let registry = loader.registry();
487        assert_eq!(registry.count(), 2);
488        assert!(registry.exists("operate"));
489    }
490
491    #[test]
492    fn test_autoload_accessor() {
493        let (_tmp, loader) = make_test_env();
494        loader.register().unwrap();
495
496        let autoload = loader.autoload();
497        assert_eq!(autoload.addons_path(), loader.addons_path());
498    }
499}