# 插件开发教程(10 分钟上手) 配套示例:**`plugins/demo.hello-world/`**。本文带你从零写一个同样功能的插件,每个文件都有对照。 ## 1. 目录结构 ``` plugins/{vendor}.{name}/ # 目录名必须 vendor.name 格式(如 demo.hello-world) ├── plugin.json # 插件清单(必填) ├── src/ │ ├── ServiceProvider.php # 入口类(必填,继承基类) │ ├── Http/ # 前台控制器 │ ├── Models/ # 模型 │ └── Filament/Pages/ # 后台页面 ├── routes/web.php # 可选:前台路由(boot 时自动加载) ├── database/migrations/ # 可选:迁移(migrate 自动执行) └── views/ # 可选:视图(注册命名空间后引用) ``` ## 2. plugin.json ```json { "title": "Hello World 示例插件", "version": "1.0.0", "description": "一句话描述", "usage": "使用说明(多行文本,后台插件卡片可折叠查看)", "author": "你的名字", "type": "demo", "provider": "Plugins\\Demo\\HelloWorld\\ServiceProvider", "requires": ["neatstudio.payment"], // 可选:依赖的插件 "filament_pages": ["Plugins\\Demo\\HelloWorld\\Filament\\Pages\\HelloWorldSettings"], "filament_resources": ["...\\Resources\\XxxResource"] } ``` 字段说明见 `docs/plugins.md`。`provider` 指向入口类;缺省时系统会自动推断 `src/ServiceProvider.php`。 ## 3. 入口类 ServiceProvider ```php loadRoutes(__DIR__.'/../routes/web.php'); // 加载路由 $this->loadViews(__DIR__.'/../views', 'demo-hello'); // 注册视图命名空间 demo-hello:: // 过滤器:文章渲染后处理(返回值传给下一个过滤器) $manager->addFilter('post.rendered', function (string $html, \App\Models\Post $post) { return $html; }, 20); // 动作:监听事件(无返回值) $manager->addAction('comment.created', function (\App\Models\Comment $comment) { // 做点什么 }); } } ``` > 入口类**不要**命名为 `PluginServiceProvider`(与基类短名冲突),统一用 `ServiceProvider`。 > **类自动加载**:系统启动时自动扫描 `plugins/*/src` 并按 `Plugins\{Vendor}\{Name}` 注册 PSR-4——**无需修改 composer.json**,第三方 ZIP 安装的插件同样生效。 ### 常用钩子 | Hook | 类型 | 参数 | 用途 | |------|------|------|------| | `post.rendered` | filter | `(string $html, Post) : string` | 文章渲染后处理(短代码/付费过滤) | | `comment.created` | action | `Comment` | 新评论创建 | | `payment.paid` | action | `Payment` | 支付成功(按 `$payment->payable` 分发) | | `filament.post_form` | action | `Schema` | 文章编辑表单追加字段 | | `filament.post_table` | action | `Table` | 文章列表追加列 | | `seo.structured_data` | filter | `(array $data) : array` | 扩展 JSON-LD | 完整列表见 `docs/plugins.md`。 ## 4. 短代码示例(过滤器) ```php $manager->addFilter('post.rendered', function (string $html, Post $post) { $greeting = (string) \App\Models\Setting::get('hello_world_greeting', '你好'); return preg_replace_callback( '/\[hello\s+([^\]]+)\]/', fn (array $m) => ''.$greeting.','.e($m[1]).'!', $html ); }, 20); ``` 文章里写 `[hello 世界]`,渲染时变成「你好,世界!」。`e()` 转义用户输入防 XSS。 ## 5. 路由 + 视图 `routes/web.php`: ```php Route::get('/hello-world', [HelloWorldController::class, 'index'])->name('hello-world.index'); ``` 控制器返回插件视图(命名空间 `demo-hello::`): ```php return view('demo-hello::hello', compact('logs')); ``` 视图里可以 `@include('partials.header')` 复用当前主题的公共片段。 ## 6. 迁移 + 模型 迁移放在 `database/migrations/`,**无需手动 --path**,`php artisan migrate` 自动执行(AppServiceProvider 已注册 `plugins/*/database/migrations`)。 模型照常写,命名空间 `Plugins\Demo\HelloWorld\Models`。 ## 7. 后台页面 `src/Filament/Pages/HelloWorldSettings.php`(继承 `Filament\Pages\Page` + `InteractsWithForms`),在 `plugin.json` 的 `filament_pages` 注册后自动出现在「插件」导航分组。可复用默认表单页视图 `filament.pages.plugin-settings`(见示例)。 ## 8. 打包与安装 ```bash cd plugins/demo.hello-world && zip -r ../demo.hello-world.zip . -x ".*" ``` 后台「插件管理」→ 上传 ZIP 安装 → 启用。目录名即包键,重复安装会提示已存在。 ## 9. 测试你的插件 ```bash # 快速验证短代码 php artisan tinker --execute=" \Plugins\Demo\HelloWorld\ServiceProvider::bootPlugin(app(\App\Blog\Support\PluginManager::class)); \$html = '正文 [hello 世界]'; echo app(\App\Blog\Support\PluginManager::class)->applyFilters('post.rendered', \$html, \App\Models\Post::first()); " ``` 建议为插件写 Feature 测试(参照 `tests/Feature/` 现有用例)。 ## 常见问题 - **改了代码不生效**:后台启停一次,或重启队列消费者(workerman/dev:watch) - **依赖其他插件**:在 `requires` 声明,系统会强制校验(依赖未启用无法启用) - **想让商品出现在订单页**:实体实现 `payableLabel()` / `payableUrl()` 两个方法(见 `docs/plugins.md`「订单商品化」) - **不想污染核心**:后台表单/表格一律走 `filament.post_form` / `filament.post_table` 钩子注入,核心零改动