# 插件开发教程（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
<?php

declare(strict_types=1);

namespace Plugins\Demo\HelloWorld;

use App\Blog\Support\PluginManager;
use App\Blog\Support\PluginServiceProvider;

class ServiceProvider extends PluginServiceProvider
{
    protected function boot(PluginManager $manager): void
    {
        $this->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) => '<span class="hello-world">'.$greeting.'，'.e($m[1]).'！</span>',
        $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` 钩子注入，核心零改动
