Files
laralog/docs/tutorial-plugin.md
ak 8fdf8dbb4b docs: 开发规范/教程/demo 全套 + 修复插件运行期自动加载缺口
- demo 插件 plugins/demo.hello-world:演示短代码过滤器、comment.created 钩子、路由+命名空间视图、迁移+模型、后台设置页、filament.post_form 表单注入
- demo 主题 themes/demo:最小可运行主题(覆盖 partials + 绿色系样式)
- 教程 docs/tutorial-plugin.md / docs/tutorial-theme.md(10 分钟上手);规范 docs/development.md(代码/插件/主题/队列/测试/提交)
- README 增加开发者文档导航与上手示例
- 修复关键缺口:插件类原来靠 composer.json 硬编码加载,第三方 ZIP 安装的插件无法加载;现改为启动时扫描 plugins/*/src 运行期注册 PSR-4(register 阶段,保证 Filament 面板解析插件页面前就绪),composer.json 移除硬编码
- 测试:demo 插件 3 个用例 + demo 主题渲染;全量 89 通过
2026-08-12 17:26:34 +08:00

5.8 KiB
Raw Permalink Blame History

插件开发教程(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

{
    "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.mdprovider 指向入口类;缺省时系统会自动推断 src/ServiceProvider.php

3. 入口类 ServiceProvider

<?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. 短代码示例(过滤器)

$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

Route::get('/hello-world', [HelloWorldController::class, 'index'])->name('hello-world.index');

控制器返回插件视图(命名空间 demo-hello::):

return view('demo-hello::hello', compact('logs'));

视图里可以 @include('partials.header') 复用当前主题的公共片段。

6. 迁移 + 模型

迁移放在 database/migrations/无需手动 --pathphp artisan migrate 自动执行(AppServiceProvider 已注册 plugins/*/database/migrations)。

模型照常写,命名空间 Plugins\Demo\HelloWorld\Models

7. 后台页面

src/Filament/Pages/HelloWorldSettings.php(继承 Filament\Pages\Page + InteractsWithForms),在 plugin.jsonfilament_pages 注册后自动出现在「插件」导航分组。可复用默认表单页视图 filament.pages.plugin-settings(见示例)。

8. 打包与安装

cd plugins/demo.hello-world && zip -r ../demo.hello-world.zip . -x ".*"

后台「插件管理」→ 上传 ZIP 安装 → 启用。目录名即包键,重复安装会提示已存在。

9. 测试你的插件

# 快速验证短代码
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 钩子注入,核心零改动