# 插件开发指南

## 目录格式
```
plugins/{vendor}/{name}/
  plugin.json
  README.md              # 默认 / 英文
  README.zh_CN.md        # 可选，后台中文界面优先
  database/migrations/   # 可选
  resources/views/       # 可选
  src/PluginServiceProvider.php
```

### plugin.json
```json
{
  "name": "vendor/my-plugin",
  "title": "显示名",
  "version": "1.0.0",
  "description": "说明",
  "provider": "Plugins\\Vendor\\MyPlugin\\PluginServiceProvider",
  "requires": ["larablog/payment"],
  "optional": [],
  "docs": "README.md"
}
```

- `requires`：硬依赖，未启用时 `PluginManager::enable` 会拒绝
- `docs`：相对插件根目录的说明文件（默认 `README.md`）。后台按当前语言优先读 `README.{locale}.md`（如 `README.zh_CN.md`），再试语言前缀（如 `README.zh.md`），最后回退到 `docs` 指定文件。Markdown 会渲染成 HTML，在侧栏弹层中展示。只允许指向插件目录内（`..`、绝对路径、软链越界会被拒绝）

在 `composer.json` → `autoload.psr-4` 注册命名空间，然后：

```bash
composer dump-autoload
php artisan plugins:sync
# 启用后如有迁移：
php artisan migrate
```

## 生命周期
1. `PluginManager::discover()` 扫描磁盘
2. `plugins:sync` / 后台同步写入 `plugins` 表
3. 启用后 `registerEnabledProviders()` 注册 Provider（Filament panel 构建前也会注册，以便 `Panel::configureUsing`）
4. Provider `register()` / `boot()` 内挂 Hook、路由、Filament Resource、迁移

## Hook 类型
| 方法 | 用途 |
|---|---|
| `listen` + `dispatch` | 事件通知（如 `comment.created`、`order.paid`） |
| `gather` | 字符串管道（主题槽 `theme.*`） |
| `collect` | 数组合并（Filament 组件/列/动作） |
| `filter` | 值管道（表单 mutate 等；每个监听器可替换值） |
| `listeners` | 取回监听器自行折叠（`article.access` 用它保证只许变严） |

```php
use App\Domain\Plugin\Hook;

Hook::listen('comment.created', function (Comment $comment): void { /* ... */ });
Hook::listen('theme.sidebar', function (string $html): string {
    return $html.'<p>侧栏注入</p>';
});
Hook::listen('filament.article.form', function (array $components): array {
    return [/* Section / Field */];
});
Hook::listen('article.access', function (AccessDecision $decision, array $ctx): AccessDecision {
    return $decision->tightenWith(/* 更严决策 */);
});
```

## 约定扩展点（首批）
| 点 | 类型 | 用途 |
|---|---|---|
| `theme.*` | gather | 前台主题槽 |
| `article.access` | fold | 收紧阅读权限；核心逐个 `tightenWith` 折叠，放宽无效、非 `AccessDecision` 返回值忽略 |
| `filament.article.form` | collect | 文章表单追加组件 |
| `filament.article.table.columns` | collect | 文章表追加列 |
| `filament.article.table.filters` | collect | 文章表追加筛选 |
| `filament.article.table.query` | filter | 文章表查询（如子查询列） |
| `filament.article.actions` | collect | 文章动作 |
| `filament.article.mutate_before_fill` | filter | 编辑回填 |
| `filament.article.mutate_before_save` | filter | 保存前改数据（勿在此 unset 限制字段） |
| `filament.article.validate_access_restrictions` | filter | 密码/付费/会员互斥校验（strip 之前） |
| `filament.article.after_save` | dispatch | 保存后写插件表 |
| `order.paid` / `order.refunded` | dispatch | 支付插件发出 |

## 三类注入
1. **UI**：前台主题槽 + 后台 Filament collect
2. **功能**：access / lifecycle Hook
3. **数据**：插件自有 migration/model，用外键或 `product_type`+`product_id` 关联

## Filament 页面/资源
放在插件命名空间，于 `register()` 中：

```php
Panel::configureUsing(function (Panel $panel): void {
    if ($panel->getId() !== 'admin') return;
    $panel->resources([OrderResource::class])->pages([SettingsPage::class]);
});
```

Resource / Page 需用 `canAccess` / `shouldRegisterNavigation` 检查插件已启用。

## 后台
- 「插件」页：卡片启停、依赖展示、使用说明
- 可选：继续用核心 `PluginSkeletonPage` 做极简占位

## 内置插件
| 插件 | 状态 |
|---|---|
| `larablog/ai-comment-moderation` | 可运行 |
| `larablog/payment` | Stub 订单/权益可跑通 |
| `larablog/paid-content` | 文章付费（依赖 payment） |
| `larablog/membership` | 会员套餐 / Stub 订阅 / 会员可见文章（依赖 payment） |
| `larablog/plugin-marketplace` | 骨架 |
| `larablog/theme-marketplace` | 骨架 |
