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 通过
This commit is contained in:
ak
2026-08-12 17:26:34 +08:00
parent 7364def979
commit 8fdf8dbb4b
21 changed files with 840 additions and 11 deletions
+85
View File
@@ -0,0 +1,85 @@
# 开发规范
面向所有为 LaraLog 写代码的人(核心 / 插件 / 主题作者)。教程见 `tutorial-plugin.md``tutorial-theme.md`,配套示例见 `plugins/demo.hello-world/``themes/demo/`
## 1. 代码规范
- **PHP 8.2+**,所有文件以 `declare(strict_types=1);` 开头
- 命名空间:核心 `App\...`;插件 `Plugins\{Vendor}\{Name}\...`Vendor/Name 用 `Str::studly` 规则)
- 文件命名:类文件与类名一致(PSR-4 自动加载)
- 方法/属性/变量:`camelCase`;常量:`UPPER_SNAKE`;数据库表:`snake_case`
- 中文文案直接写在代码/语言包中(本项目默认中文,不硬编码英文 UI)
- 不写无意义注释;只在「为什么」不明显处加一行注释
- **安全红线**:不提交 `.env`、密钥、证书;用户输入一律 `e()` 转义或框架验证;数据库操作用 Eloquent/查询构造器(防注入)
## 2. 目录约定
```
app/Blog/ # 前台域模块:Controllers / Jobs / Services / Support / View
app/Filament/ # 后台资源与页面
plugins/ # 插件,目录名 vendor.name
themes/ # 主题,目录名即主题名
routes/web.php # 前台规范路由;routes/legacy.php 老 URL 301(新增老入口兼容放这里)
```
## 3. 插件规范(详见 docs/plugins.md
- 目录 `plugins/{vendor}.{name}/`**目录名是插件唯一标识**
- 类自动加载:系统启动时扫描 `plugins/*/src` 注册 PSR-4`Plugins\{Vendor}\{Name}`),**无需改 composer.json**
- `plugin.json` 必填:`title/version/description/author/type/provider`;可选 `usage/requires/filament_pages/filament_resources`
- 入口类 `src/ServiceProvider.php` 继承 `App\Blog\Support\PluginServiceProvider`**不要命名为 PluginServiceProvider**
- 依赖声明在 `requires``vendor.name`),系统强制校验:依赖未启用不能启用本插件;停用/卸载被依赖者阻止
- 后台扩展**一律走钩子注入**`filament.post_form` / `filament.post_table`),禁止改核心资源文件
- 可购买实体约定:实现 `payableLabel(): string``payableUrl(): ?string` 即接入订单展示(支付插件零改动)
- 插件视图放插件目录并用 `loadViews()` 注册命名空间;前台页面用 `@include('partials.header')` 复用主题公共片段
### 钩子清单
| Hook | 类型 | 参数 | 用途 |
|------|------|------|------|
| `post.rendered` | filter | `(string $html, Post): string` | 文章渲染后处理 |
| `comment.created` | action | `Comment` | 新评论创建 |
| `payment.paid` | action | `Payment` | 支付成功分发 |
| `filament.post_form` | action | `Schema` | 文章表单注入 |
| `filament.post_table` | action | `Table` | 文章表格注入 |
| `seo.structured_data` | filter | `(array): array` | JSON-LD 扩展 |
| `theme.inject.{point}` | filter | `(string): string` | 注入点追加 HTML |
> `Schema::components()` / `Table::columns()` 是**替换**语义:表单用 `[...$schema->getComponents(), Section::make(...)]` 合并,表格用 `pushColumns()`
## 4. 主题规范(详见 docs/themes.md
- 目录 `themes/{name}/``theme.json` 必填;views 前置到全局视图路径,同名覆盖、缺失回退
- 语义化 HTMLheader/nav/main/aside/footer/article/time)是 SEO 要求,不是可选项
- 保留注入点 `@stack('theme:xxx')`,并尽量在 theme.json 声明 `inject_points`
- 必须有移动端断点(≤768px 两栏改单栏)
- 资产开发期流式返回、生产 `theme:publish`
## 5. 队列任务规范
- 实现 `App\Blog\Jobs\AiJob` 接口(`handle(LlmClient)` 由容器注入)+ `ShouldQueue`
- 入队 `dispatch(new XxxJob($id))->onQueue('ai')`
- 消费者二选一:`php artisan workerman:serve`(常驻+WS)或 `php artisan queue:work --queue=default,ai`(等价,见 docs/deploy.md
- LLM 一律走 `LlmClient`OpenAI 兼容,base_url/key/model 可配),不要直连第三方 SDK
## 6. 测试规范
- 新增功能必须配 Feature 测试(`tests/Feature/`),跑 `php artisan test` 全绿再提交
- 示例参照:`PluginDependencyTest`(依赖校验)、`PostFormInjectionTest`(表单注入)、`MarketPurchaseTest`(商城购买流)、`MembershipFlowTest`(会员/支付流)
- 测试用 RefreshDatabase + 沙箱,**不触碰真实支付/真实 LLM 凭据**
## 7. 提交规范
- 一次提交一件事,信息用中文描述「做了什么 + 为什么」
- 不提交:`.env`、密钥/证书、`storage/media-library/temp/`、构建产物
- 完成后更新对应 `docs/` 文档
## 8. 常用命令速查
```bash
php artisan test # 全量测试
php artisan dev:watch # 开发热重载(改插件/核心代码自动重启 workerman)
php artisan migrate # 插件迁移自动加载
php artisan workerman:serve start # 队列消费者 + WebSocket
php artisan theme:publish # 主题资产发布到 public
```
+156
View File
@@ -0,0 +1,156 @@
# 插件开发教程(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` 钩子注入,核心零改动
+97
View File
@@ -0,0 +1,97 @@
# 主题开发教程(10 分钟上手)
配套示例:**`themes/demo/`**。本文用最小结构带你做一个能用的主题。
## 1. 最小主题结构
```
themes/demo/
├── theme.json # 主题清单(必填)
├── views/
│ └── partials/ # 覆盖公共片段
│ ├── head.blade.php
│ ├── header.blade.php
│ └── footer.blade.php
└── assets/
└── style.css # 主题样式(/themes/demo/assets/style.css 访问)
```
**核心机制**:激活主题的 `views/` 目录被前置到全局视图路径。页面视图(`index`/`show`/`list`…)按名字解析——主题有同名视图就用主题的,**没有就自动回退默认视图**(`resources/views/`)。
所以最小主题只需要覆盖 `partials/`head/header/sidebar/footer),页面样式自己定,其他全部继承默认。
## 2. theme.json
```json
{
"title": "Demo 演示主题",
"version": "1.0.0",
"description": "一句话描述",
"author": "你的名字",
"screenshot": "screenshot.png",
"inject_points": ["head", "header", "footer"]
}
```
- `screenshot``assets/` 下的预览图,后台主题卡片展示(可选)
- `inject_points`:声明实现了哪些注入点(后台会展示);缺省视为全部支持
## 3. 覆盖 partials
`views/partials/head.blade.php` 需要包含:`<title>`、meta、CSS 引用。**必须保留注入点** `@stack('theme:head')`(后台「主题注入」的内容会渲染到这里):
```blade
<link rel="stylesheet" href="{{ theme('asset', 'style.css') }}">
@stack('head')
@stack('theme:head')
```
`header.blade.php` / `footer.blade.php` 同理,尾部保留 `@stack('theme:footer')` / `@stack('theme:body_end')`
## 4. 视图解析规则
| 场景 | 结果 |
|------|------|
| 主题有 `index.blade.php` | 覆盖默认首页 |
| 主题没有 | 用 `resources/views/index.blade.php` |
| 主题只有 partials | 页面用默认,公共片段用主题的 |
想完全自定义某类页面,就在主题 views 下放同名文件(如 `show.blade.php``tag.blade.php`),控制器传的变量清单见 `docs/themes.md`
## 5. 主题变量(可后台编辑)
`theme.json` 之外,可以在后台「主题注入 → 主题变量」里给主题加任意键值,视图用:
```blade
{{ theme('var', 'footer_about', '默认文本') }}
```
适合放广告位 HTML、自定义文案等,不用改代码。
## 6. 资产与发布
- **开发期**`GET /themes/{name}/assets/{path}` 流式返回(带缓存头),改 CSS 刷新即生效
- **生产**`php artisan theme:publish` 拷贝到 `public/themes/`,由 Web 服务器托管
## 7. 打包与安装
```bash
cd themes/demo && zip -r ../demo.zip . -x ".*"
```
后台「主题管理」→ 上传 ZIP 安装 → 启用。**不能删除当前激活的主题**。
## 8. 好主题的检查清单
- [ ] 移动端适配:`@media (max-width: 768px)` 两栏改单栏(参照 sablog/modern
- [ ] 语义化 HTML`<header>/<nav>/<main>/<aside>/<footer>/<article>/<time>`SEO 关键)
- [ ] 保留全部注入点 `@stack('theme:xxx')`
- [ ] 深色模式可选(modern 有示例)
- [ ] 文章 TOC`show` 视图接收 `$toc` 变量,样式化 `.toc` / `.toc-level-N`(可选)
- [ ] 截图 `screenshot.png`(无头浏览器截首页生成)
## 常见问题
- **页面还是旧的**:确认主题已启用、文件在 `views/` 下、缓存已清(后台「缓存管理」或 `php artisan view:clear`
- **CSS 不生效**:检查 `theme('asset', 'style.css')` 路径,生产环境要先 `theme:publish`
- **想参考完整实现**`themes/sablog/`(经典两栏)、`themes/modern/`(简约 + 深色模式)