laravel-plugins maintained by siaoynli
Laravel Plugins
一个轻量高性能的 Laravel 11 插件系统框架,支持插件自动发现、Manifest 缓存、生命周期管理和声明式资源发布。
✨ 主要特性
- 🔌 插件自动发现 — 配置文件、Composer 包、本地包三级来源
- ⚡ Manifest 缓存 — 发现结果缓存到
bootstrap/cache/plugins.php,生产环境零文件扫描 - 🔄 标准生命周期 —
register()→boot()→registerRoutes()严格分离 - 📦 声明式发布 — 基于 Laravel 原生
publishes()+vendor:publish,不再自动覆盖文件 - 🎯 延迟加载 — 构造函数零 I/O,所有属性按需初始化
- 🔔 生命周期事件 —
PluginRegistered/PluginBooted事件,支持插件间松耦合通信 - 🛣️ 路由缓存兼容 — 自动尊重
route:cache,不重复加载路由 - ✅ 启用/禁用控制 — 通过配置文件控制每个插件的启用状态
- 💻 Artisan 命令 —
plugin:list/plugin:publish/plugin:cache/plugin:clear
📋 目录
📦 安装
前置要求
- PHP >= 8.2
- Laravel >= 11.0
安装步骤
composer require siaoynli/laravel-plugins
Laravel 11 的包自动发现会自动注册 PluginServiceProvider,无需手动配置。
发布配置文件(可选)
php artisan vendor:publish --tag=laravel-plugins-config
这会在 config/ 目录下生成 plugins.php 配置文件。
构建缓存(推荐)
php artisan plugin:cache
生产环境构建缓存后,后续请求不再扫描文件系统。
🚀 快速开始
1. 查看已加载的插件
php artisan plugin:list
输出示例:
1 plugin(s) loaded:
┌──────────────────────────┬──────────────┬─────────┬─────────┐
│ Package │ Name │ Version │ Enabled │
├──────────────────────────┼──────────────┼─────────┼─────────┤
│ siaoynli/phone-auth │ 手机验证码登录 │ 1.0.8 │ ✓ │
└──────────────────────────┴──────────────┴─────────┴─────────┘
2. 发布插件资源
# 查看可用的发布标签
php artisan plugin:publish
# 使用 Laravel 原生命令发布
php artisan vendor:publish --tag=siaoynli-phone-auth-plugin-config
php artisan vendor:publish --tag=siaoynli-phone-auth-plugin-migrations
php artisan vendor:publish --tag=siaoynli-phone-auth-plugin-views
php artisan vendor:publish --tag=siaoynli-phone-auth-plugin-assets
📝 创建插件
步骤 1: 创建目录结构
packages/my-vendor/my-plugin/
├── composer.json
├── config/
│ └── plugin.php # 插件配置(可选)
├── src/
│ ├── MyPluginPlugin.php # 插件主类
│ └── Providers/ # 服务提供者(可选,自动发现)
├── routes/
│ └── api.php # 路由文件(可选)
├── database/
│ └── migrations/ # 迁移文件(可选)
└── resources/
├── views/ # 视图文件(可选)
└── assets/ # 静态资源(可选)
步骤 2: 创建 composer.json
{
"name": "my-vendor/my-plugin",
"description": "My awesome plugin",
"version": "1.0.0",
"type": "library",
"require": {
"php": "^8.2",
"laravel/framework": "^11.0",
"siaoynli/laravel-plugins": "^1.0"
},
"autoload": {
"psr-4": {
"MyVendor\\MyPlugin\\": "src/"
}
},
"extra": {
"plugin": {
"class": "MyVendor\\MyPlugin\\MyPluginPlugin"
}
}
}
extra.plugin.class告诉插件系统哪个类是插件入口。如果不配置,系统会尝试根据 PSR-4 命名空间自动推断。
步骤 3: 创建插件主类
<?php
namespace MyVendor\MyPlugin;
use Siaoynli\Plugins\AbstractPlugin;
class MyPluginPlugin extends AbstractPlugin
{
/**
* register() — 仅做容器绑定和配置合并
* 默认实现已自动合并 config/plugin.php 并注册 src/Providers/ 下的服务提供者
*/
public function register(): void
{
parent::register();
// 绑定你自己的服务
// $this->app->singleton(MyService::class);
}
/**
* boot() — 所有服务提供者已注册,可安全使用任何服务
*/
public function boot(): void
{
// 注册事件监听、中间件等
// Event::listen(UserCreated::class, SendWelcomeEmail::class);
}
/**
* registerRoutes() — 路由注册(默认已实现,通常无需覆写)
* 自动加载 routes/*.php,应用 route_prefix 和 middleware
*/
}
关键变化:
register()和boot()现在是分离的两个阶段。register()只做容器绑定,boot()做需要依赖其他服务的初始化。
步骤 4: 创建配置文件
创建 config/plugin.php:
<?php
return [
'enabled' => true,
'route_prefix' => 'my-plugin',
'middleware' => ['api'],
];
步骤 5: 创建路由
创建 routes/api.php:
<?php
use Illuminate\Support\Facades\Route;
Route::get('/status', function () {
return response()->json([
'plugin' => 'my-plugin',
'status' => 'ok',
]);
});
路由会自动包裹在 Route::group 中,应用配置的前缀和中间件。访问路径为 /my-plugin/status。
步骤 6: 测试插件
# 清除缓存并重新发现
php artisan plugin:clear
php artisan plugin:list
# 测试路由
curl http://localhost:8000/my-plugin/status
# {"plugin":"my-plugin","status":"ok"}
🔍 插件发现与缓存
三级发现来源(按优先级)
| 优先级 | 来源 | 说明 |
|---|---|---|
| 1 (最高) | config/plugins.php 的 plugins 数组 |
手动注册,不会被自动发现覆盖 |
| 2 | vendor/composer/installed.json |
Composer 安装的包,需声明 extra.plugin.class |
| 3 (最低) | packages/ 目录扫描 |
本地开发包,最多扫描 2 层目录 |
Manifest 缓存
插件发现结果会缓存到 bootstrap/cache/plugins.php:
// bootstrap/cache/plugins.php
return [
'my-vendor/my-plugin' => [
'class' => 'MyVendor\\MyPlugin\\MyPluginPlugin',
'source' => 'local',
],
'siaoynli/phone-auth-plugin' => [
'class' => 'Siaoynli\\PhoneAuth\\PhoneAuthPlugin',
'source' => 'vendor',
],
];
缓存命中时:直接 require 缓存文件,零文件系统扫描,单次请求开销约 1ms。
# 构建缓存
php artisan plugin:cache
# 清除缓存(开发时常用)
php artisan plugin:clear
生产环境建议:部署时执行
plugin:cache,开发时关闭缓存或将PLUGINS_SCAN_LOCAL保持开启。
🔄 插件生命周期
插件系统严格遵循 Laravel 的 ServiceProvider 生命周期:
ServiceProvider::register()
└── 绑定 PluginManifest / PluginDiscovery / PluginRegistry / PluginPublisher 单例
ServiceProvider::boot()
│
├── 1. Discovery — 发现插件(有缓存则跳过扫描)
│
├── 2. Register — 实例化插件 → 调用 register()
│ ├── 合并配置(应用配置优先)
│ ├── 自动注册 src/Providers/ 下的服务提供者
│ └── 分发 PluginRegistered 事件
│
├── 3. Boot — 调用 boot() + registerRoutes()
│ ├── 子类自定义启动逻辑
│ ├── 加载 routes/*.php(尊重路由缓存)
│ └── 分发 PluginBooted 事件
│
└── 4. Publish — 声明式注册可发布资源(不拷贝文件)
监听生命周期事件
// AppServiceProvider::boot()
use Siaoynli\Plugins\Events\PluginRegistered;
use Siaoynli\Plugins\Events\PluginBooted;
Event::listen(function (PluginRegistered $event) {
Log::info("插件已注册: {$event->plugin->getName()}");
});
Event::listen(function (PluginBooted $event) {
Log::info("插件已启动: {$event->plugin->getName()}");
});
📦 资源发布
资源发布采用 Laravel 标准的声明式注册模式:
boot()时只注册哪些资源可以发布,不拷贝文件- 用户通过
vendor:publish命令手动触发发布
发布标签
| 标签 | 来源 | 目标 |
|---|---|---|
{tag}-migrations |
database/migrations/ |
database/migrations/ |
{tag}-config |
config/plugin.php |
config/plugins/{slug-name}.php |
{tag}-views |
resources/views/ |
resources/views/vendor/{slug-name}/ |
{tag}-assets |
resources/assets/ |
public/plugins/{slug-name}/ |
其中 {tag} 为插件包名中 / 替换为 -,例如 siaoynli/phone-auth-plugin → siaoynli-phone-auth-plugin。
发布命令
# 查看某个插件的可用标签
php artisan plugin:publish siaoynli/phone-auth-plugin
# 发布指定类型
php artisan vendor:publish --tag=siaoynli-phone-auth-plugin-config
php artisan vendor:publish --tag=siaoynli-phone-auth-plugin-migrations
# 发布某个插件的所有资源
php artisan vendor:publish --tag=siaoynli-phone-auth-plugin-config \
--tag=siaoynli-phone-auth-plugin-migrations \
--tag=siaoynli-phone-auth-plugin-views
# 发布所有插件的所有资源
php artisan vendor:publish --provider="Siaoynli\Plugins\Providers\PluginServiceProvider"
# 强制覆盖已有文件
php artisan vendor:publish --tag=siaoynli-phone-auth-plugin-config --force
⚙️ 配置参考
发布配置文件后,可在 config/plugins.php 中调整:
return [
// 插件系统总开关
'enabled' => env('PLUGINS_ENABLED', true),
// 插件发现配置
'discovery' => [
'scan_local_packages' => env('PLUGINS_SCAN_LOCAL', true), // 是否扫描 packages/ 目录
'packages_path' => 'packages', // 本地包目录
],
// Manifest 缓存配置
'cache' => [
'enabled' => env('PLUGIN_CACHE', true),
'path' => null, // null = bootstrap/cache/plugins.php
],
// 手动注册插件(最高优先级)
'plugins' => [
// 'vendor/package-name' => \Vendor\Package\PluginClass::class,
],
];
插件级配置
插件的配置统一通过 config/plugins/{slug-name}.php 管理({slug-name} 为包名中 / 替换为 -)。
获取配置文件:通过 vendor:publish 发布插件配置:
php artisan vendor:publish --tag=my-vendor-my-plugin-config
这会将插件自带的 config/plugin.php 复制到 config/plugins/my-vendor-my-plugin.php。
编辑配置:
// config/plugins/my-vendor-my-plugin.php
return [
'enabled' => true, // 是否启用
'route_prefix' => 'my-plugin', // 路由前缀
'middleware' => ['api'], // 路由中间件
// 插件自定义的其他配置项...
];
配置优先级:config/plugins/{slug-name}.php(应用级) > 插件自带 config/plugin.php(默认值)。register() 阶段会自动将两者合并,应用级配置覆盖同名键。
🔧 API 参考
PluginInterface(契约)
所有插件必须实现 7 个核心方法:
interface PluginInterface
{
public function getName(): string; // 插件名称
public function getVersion(): string; // 版本号
public function getDescription(): string; // 描述
public function isEnabled(): bool; // 是否启用
public function register(): void; // 注册阶段(容器绑定)
public function boot(): void; // 启动阶段(路由、事件等)
public function registerRoutes(): void; // 注册路由
}
AbstractPlugin(基类)
继承 AbstractPlugin 可获得所有默认实现,通常只需覆写 boot():
use Siaoynli\Plugins\AbstractPlugin;
class MyPlugin extends AbstractPlugin
{
// register() — 已实现:配置合并 + 服务提供者自动发现
// boot() — 空实现,子类按需覆写
// registerRoutes() — 已实现:自动加载 routes/*.php
// 可用的公共方法:
// $this->getBasePath() → 插件根目录
// $this->getConfig(?$key) → 获取配置
// $this->getRoutePrefix() → 路由前缀
// $this->getMiddleware() → 中间件列表
// $this->getPluginNamespace() → PSR-4 命名空间
}
PluginRegistry(注册表)
推荐使用 PluginRegistry 直接操作插件实例:
use Siaoynli\Plugins\Registry\PluginRegistry;
// 依赖注入
public function __construct(PluginRegistry $registry)
{
// 获取所有插件
$plugins = $registry->all();
// 获取特定插件
$plugin = $registry->get('vendor/plugin-name');
// 检查是否存在
$exists = $registry->has('vendor/plugin-name');
// 获取摘要列表
$list = $registry->list();
// 插件数量
$count = $registry->count();
}
PluginManager(向后兼容门面)
PluginManager 保留为 PluginRegistry 的门面,旧代码无需修改:
use Siaoynli\Plugins\PluginManager;
$manager = app(PluginManager::class); // 或 app('plugin-manager')
$plugins = $manager->getPlugins(); // → $registry->all()
$plugin = $manager->getPlugin($name); // → $registry->get($name)
$exists = $manager->hasPlugin($name); // → $registry->has($name)
$list = $manager->listPlugins(); // → $registry->list()
提示:新代码建议直接注入
PluginRegistry。
💻 Artisan 命令
plugin:list
列出所有已加载的插件。
php artisan plugin:list
plugin:publish
显示插件的可用发布标签和使用示例。
# 查看所有插件的发布标签
php artisan plugin:publish
# 查看特定插件的发布标签
php artisan plugin:publish siaoynli/phone-auth-plugin
plugin:cache
构建插件 manifest 缓存,生产环境推荐。
php artisan plugin:cache
plugin:clear
清除 manifest 缓存。
php artisan plugin:clear
❓ 常见问题
Q: 如何禁用某个插件?
在 config/plugins/{slug-name}.php 中设置 enabled 为 false:
// config/plugins/siaoynli-phone-auth-plugin.php
return [
'enabled' => false,
];
如果该文件不存在,先发布配置:
php artisan vendor:publish --tag=siaoynli-phone-auth-plugin-config
### Q: 如何自定义路由前缀和中间件?
在 `config/plugins/{slug-name}.php` 中配置:
```php
// config/plugins/my-vendor-my-plugin.php
return [
'route_prefix' => 'custom-prefix',
'middleware' => ['api', 'auth:api'],
];
### Q: 插件未被加载怎么办?
1. 清除缓存:`php artisan plugin:clear`
2. 检查 `config/plugins.php` 的 `plugins` 数组
3. 确认插件 `composer.json` 中有 `extra.plugin.class` 配置
4. 运行 `php artisan plugin:list` 查看加载状态
5. 检查日志 `storage/logs/laravel.log`
### Q: 开发时修改了插件但没生效?
```bash
php artisan plugin:clear # 清除 manifest 缓存
php artisan config:clear # 清除配置缓存
Q: 路由缓存后插件路由消失了?
AbstractPlugin::registerRoutes() 已内置 routesAreCached() 检查。如果你自己覆写了 registerRoutes(),请确保加入同样的检查:
public function registerRoutes(): void
{
if (app()->routesAreCached()) return;
// 你的路由注册逻辑...
}
Q: 如何在插件中使用视图?
插件的 resources/views/ 会被自动注册为命名空间视图:
// 插件内
return view('siaoynli-phone-auth-plugin::login');
// 发布后用户可覆盖
// resources/views/vendor/siaoynli-phone-auth-plugin/login.blade.php
Q: 如何监听其他插件的生命周期?
use Siaoynli\Plugins\Events\PluginBooted;
Event::listen(function (PluginBooted $event) {
if ($event->plugin->getName() === 'siaoynli/phone-auth-plugin') {
// 在特定插件启动后执行逻辑
}
});
Q: 本地包和 Vendor 包有什么区别?
- 本地包(
packages/)— 用于开发阶段,便于快速迭代 - Vendor 包(
vendor/)— 生产环境,通过 Composer 安装
两者使用相同的发现机制,区别仅在于目录位置。
🤝 贡献
欢迎提交 Issue 和 Pull Request!
📄 许可证
本项目采用 MIT 许可证。详见 LICENSE 文件。
🎯 Roadmap
- 插件市场/仓库
- 插件依赖管理与加载顺序
- 插件权限控制
- 前端插件支持
- 插件配置 UI
-
插件事件系统✅ 已实现(PluginRegistered / PluginBooted) -
Manifest 缓存✅ 已实现(plugin:cache / plugin:clear)
📮 联系方式
如有问题或建议,请提交 Issue。
Made with ❤️ by Siaoynli