chore(plugin): Refactor plugin architecture and improve code quality

This commit is contained in:
xboard 2025-02-09 22:38:09 +08:00
parent 0f80ab8d5f
commit ebd5c09145
4 changed files with 350 additions and 73 deletions

View File

@ -40,9 +40,10 @@ class PluginController extends Controller
$configFile = $directory . '/config.json'; $configFile = $directory . '/config.json';
if (File::exists($configFile)) { if (File::exists($configFile)) {
$config = json_decode(File::get($configFile), true); $config = json_decode(File::get($configFile), true);
$installed = isset($installedPlugins[$pluginName]); $code = $config['code'];
$installed = isset($installedPlugins[$code]);
// 使用配置服务获取配置 // 使用配置服务获取配置
$pluginConfig = $installed ? $this->configService->getConfig($pluginName) : ($config['config'] ?? []); $pluginConfig = $installed ? $this->configService->getConfig($code) : ($config['config'] ?? []);
$plugins[] = [ $plugins[] = [
'code' => $config['code'], 'code' => $config['code'],
'name' => $config['name'], 'name' => $config['name'],
@ -50,7 +51,7 @@ class PluginController extends Controller
'description' => $config['description'], 'description' => $config['description'],
'author' => $config['author'], 'author' => $config['author'],
'is_installed' => $installed, 'is_installed' => $installed,
'is_enabled' => $installed ? $installedPlugins[$pluginName]['is_enabled'] : false, 'is_enabled' => $installed ? $installedPlugins[$code]['is_enabled'] : false,
'config' => $pluginConfig, 'config' => $pluginConfig,
]; ];
} }

View File

@ -5,6 +5,7 @@ namespace App\Services\Plugin;
use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
abstract class AbstractPlugin abstract class AbstractPlugin
@ -12,14 +13,40 @@ abstract class AbstractPlugin
protected array $config = []; protected array $config = [];
protected string $basePath; protected string $basePath;
protected string $pluginCode; protected string $pluginCode;
protected string $namespace;
public function __construct($pluginCode) public function __construct(string $pluginCode)
{ {
$this->pluginCode = $pluginCode; $this->pluginCode = $pluginCode;
$this->namespace = 'Plugin\\' . Str::studly($pluginCode);
$reflection = new \ReflectionClass($this); $reflection = new \ReflectionClass($this);
$this->basePath = dirname($reflection->getFileName()); $this->basePath = dirname($reflection->getFileName());
} }
/**
* 获取插件代码
*/
public function getPluginCode(): string
{
return $this->pluginCode;
}
/**
* 获取插件命名空间
*/
public function getNamespace(): string
{
return $this->namespace;
}
/**
* 获取插件基础路径
*/
public function getBasePath(): string
{
return $this->basePath;
}
/** /**
* 设置配置 * 设置配置
*/ */
@ -36,6 +63,14 @@ abstract class AbstractPlugin
return $this->config; return $this->config;
} }
/**
* 获取视图
*/
protected function view(string $view, array $data = [], array $mergeData = []): \Illuminate\Contracts\View\View
{
return view(Str::studly($this->pluginCode) . '::' . $view, $data, $mergeData);
}
/** /**
* 注册动作钩子监听器 * 注册动作钩子监听器
*/ */
@ -70,4 +105,76 @@ abstract class AbstractPlugin
{ {
HookManager::intercept($response); HookManager::intercept($response);
} }
/**
* 插件启动时调用
*/
public function boot(): void
{
// 插件启动时的初始化逻辑
}
/**
* 插件安装时调用
*/
public function install(): void
{
// 插件安装时的初始化逻辑
}
/**
* 插件卸载时调用
*/
public function uninstall(): void
{
// 插件卸载时的清理逻辑
}
/**
* 插件更新时调用
*/
public function update(string $oldVersion, string $newVersion): void
{
// 插件更新时的迁移逻辑
}
/**
* 获取插件资源URL
*/
protected function asset(string $path): string
{
return asset('plugins/' . $this->pluginCode . '/' . ltrim($path, '/'));
}
/**
* 获取插件配置项
*/
protected function getConfigValue(string $key, $default = null)
{
return $this->config[$key] ?? $default;
}
/**
* 获取插件数据库迁移路径
*/
protected function getMigrationsPath(): string
{
return $this->basePath . '/database/migrations';
}
/**
* 获取插件视图路径
*/
protected function getViewsPath(): string
{
return $this->basePath . '/resources/views';
}
/**
* 获取插件资源路径
*/
protected function getAssetsPath(): string
{
return $this->basePath . '/resources/assets';
}
} }

View File

@ -7,6 +7,13 @@ use Illuminate\Support\Facades\File;
class PluginConfigService class PluginConfigService
{ {
protected $pluginManager;
public function __construct()
{
$this->pluginManager = app(PluginManager::class);
}
/** /**
* 获取插件配置 * 获取插件配置
* *
@ -73,7 +80,7 @@ class PluginConfigService
*/ */
protected function getDefaultConfig(string $pluginCode): array protected function getDefaultConfig(string $pluginCode): array
{ {
$configFile = base_path("plugins/{$pluginCode}/config.json"); $configFile = $this->pluginManager->getPluginPath($pluginCode) . '/config.json';
if (!File::exists($configFile)) { if (!File::exists($configFile)) {
return []; return [];
} }
@ -88,7 +95,7 @@ class PluginConfigService
* @param string $pluginCode * @param string $pluginCode
* @return array * @return array
*/ */
protected function getDbConfig(string $pluginCode): array public function getDbConfig(string $pluginCode): array
{ {
$plugin = Plugin::query() $plugin = Plugin::query()
->where('code', $pluginCode) ->where('code', $pluginCode)

View File

@ -6,22 +6,119 @@ use App\Models\Plugin;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Str;
use Symfony\Component\Finder\Finder;
class PluginManager class PluginManager
{ {
protected string $pluginPath; protected string $pluginPath;
protected array $loadedPlugins = [];
public function __construct() public function __construct()
{ {
$this->pluginPath = base_path('plugins'); $this->pluginPath = base_path('plugins');
} }
/**
* 获取插件的命名空间
*/
public function getPluginNamespace(string $pluginCode): string
{
return 'Plugin\\' . Str::studly($pluginCode);
}
/**
* 获取插件的基础路径
*/
public function getPluginPath(string $pluginCode): string
{
return $this->pluginPath . '/' . Str::studly($pluginCode);
}
/**
* 加载插件类
*/
protected function loadPlugin(string $pluginCode)
{
if (isset($this->loadedPlugins[$pluginCode])) {
return $this->loadedPlugins[$pluginCode];
}
$pluginClass = $this->getPluginNamespace($pluginCode) . '\\Plugin';
if (!class_exists($pluginClass)) {
$pluginFile = $this->getPluginPath($pluginCode) . '/Plugin.php';
if (!File::exists($pluginFile)) {
throw new \Exception("Plugin class file not found: {$pluginFile}");
}
require_once $pluginFile;
}
if (!class_exists($pluginClass)) {
throw new \Exception("Plugin class not found: {$pluginClass}");
}
$plugin = new $pluginClass($pluginCode);
$this->loadedPlugins[$pluginCode] = $plugin;
return $plugin;
}
/**
* 注册插件的服务提供者
*/
protected function registerServiceProvider(string $pluginCode): void
{
$providerClass = $this->getPluginNamespace($pluginCode) . '\\Providers\\PluginServiceProvider';
if (class_exists($providerClass)) {
app()->register($providerClass);
}
}
/**
* 加载插件的路由
*/
protected function loadRoutes(string $pluginCode): void
{
$routesPath = $this->getPluginPath($pluginCode) . '/routes';
if (File::exists($routesPath)) {
$files = ['web.php', 'api.php'];
foreach ($files as $file) {
$routeFile = $routesPath . '/' . $file;
if (File::exists($routeFile)) {
Route::middleware('web')
->namespace($this->getPluginNamespace($pluginCode) . '\\Controllers')
->group(function () use ($routeFile) {
require $routeFile;
});
}
}
}
}
/**
* 加载插件的视图
*/
protected function loadViews(string $pluginCode): void
{
$viewsPath = $this->getPluginPath($pluginCode) . '/resources/views';
if (File::exists($viewsPath)) {
View::addNamespace(Str::studly($pluginCode), $viewsPath);
}
}
/** /**
* 安装插件 * 安装插件
*/ */
public function install(string $pluginCode): bool public function install(string $pluginCode): bool
{ {
$configFile = $this->pluginPath . '/' . $pluginCode . '/config.json'; DB::beginTransaction();
try {
$configFile = $this->getPluginPath($pluginCode) . '/config.json';
if (!File::exists($configFile)) { if (!File::exists($configFile)) {
throw new \Exception('Plugin config file not found'); throw new \Exception('Plugin config file not found');
@ -32,21 +129,27 @@ class PluginManager
throw new \Exception('Invalid plugin config'); throw new \Exception('Invalid plugin config');
} }
// 检查插件是否已安装
if (Plugin::where('code', $pluginCode)->exists()) {
throw new \Exception('Plugin already installed');
}
// 检查依赖 // 检查依赖
if (!$this->checkDependencies($config['require'] ?? [])) { if (!$this->checkDependencies($config['require'] ?? [])) {
throw new \Exception('Dependencies not satisfied'); throw new \Exception('Dependencies not satisfied');
} }
// 运行数据库迁移
$this->runMigrations($pluginCode);
// 提取配置默认值 // 提取配置默认值
$defaultValues = []; $defaultValues = $this->extractDefaultConfig($config);
if (isset($config['config']) && is_array($config['config'])) {
foreach ($config['config'] as $key => $item) { // 创建插件实例
$defaultValues[$key] = $item['default'] ?? null; $plugin = $this->loadPlugin($pluginCode);
}
}
// 注册到数据库 // 注册到数据库
Plugin::create([ $dbPlugin = Plugin::create([
'code' => $pluginCode, 'code' => $pluginCode,
'name' => $config['name'], 'name' => $config['name'],
'version' => $config['version'], 'version' => $config['version'],
@ -55,6 +158,97 @@ class PluginManager
'installed_at' => now(), 'installed_at' => now(),
]); ]);
// 运行插件安装方法
if (method_exists($plugin, 'install')) {
$plugin->install();
}
// 发布插件资源
$this->publishAssets($pluginCode);
DB::commit();
return true;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 提取插件默认配置
*/
protected function extractDefaultConfig(array $config): array
{
$defaultValues = [];
if (isset($config['config']) && is_array($config['config'])) {
foreach ($config['config'] as $key => $item) {
if (is_array($item)) {
$defaultValues[$key] = $item['default'] ?? null;
} else {
$defaultValues[$key] = $item;
}
}
}
return $defaultValues;
}
/**
* 运行插件数据库迁移
*/
protected function runMigrations(string $pluginCode): void
{
$migrationsPath = $this->getPluginPath($pluginCode) . '/database/migrations';
if (File::exists($migrationsPath)) {
Artisan::call('migrate', [
'--path' => "plugins/{$pluginCode}/database/migrations",
'--force' => true
]);
}
}
/**
* 发布插件资源
*/
protected function publishAssets(string $pluginCode): void
{
$assetsPath = $this->getPluginPath($pluginCode) . '/resources/assets';
if (File::exists($assetsPath)) {
$publishPath = public_path('plugins/' . $pluginCode);
File::ensureDirectoryExists($publishPath);
File::copyDirectory($assetsPath, $publishPath);
}
}
/**
* 验证配置文件
*/
protected function validateConfig(array $config): bool
{
$requiredFields = [
'name',
'code',
'version',
'description',
'author'
];
foreach ($requiredFields as $field) {
if (!isset($config[$field]) || empty($config[$field])) {
return false;
}
}
// 验证插件代码格式
if (!preg_match('/^[a-z0-9_]+$/', $config['code'])) {
return false;
}
// 验证版本号格式
if (!preg_match('/^\d+\.\d+\.\d+$/', $config['version'])) {
return false;
}
return true; return true;
} }
@ -64,8 +258,9 @@ class PluginManager
public function enable(string $pluginCode): bool public function enable(string $pluginCode): bool
{ {
$plugin = $this->loadPlugin($pluginCode); $plugin = $this->loadPlugin($pluginCode);
if (!$plugin) { if (!$plugin) {
throw new \Exception('Plugin not found'); throw new \Exception('Plugin not found: ' . $pluginCode);
} }
// 获取插件配置 // 获取插件配置
@ -77,6 +272,15 @@ class PluginManager
$plugin->setConfig(json_decode($dbPlugin->config, true)); $plugin->setConfig(json_decode($dbPlugin->config, true));
} }
// 注册服务提供者
$this->registerServiceProvider($pluginCode);
// 加载路由
$this->loadRoutes($pluginCode);
// 加载视图
$this->loadViews($pluginCode);
// 更新数据库状态 // 更新数据库状态
Plugin::query() Plugin::query()
->where('code', $pluginCode) ->where('code', $pluginCode)
@ -85,22 +289,6 @@ class PluginManager
'updated_at' => now(), 'updated_at' => now(),
]); ]);
// 加载路由
$routesFile = $this->pluginPath . '/' . $pluginCode . '/routes/web.php';
if (File::exists($routesFile)) {
require $routesFile;
}
// 注册视图
$viewsPath = $this->pluginPath . '/' . $pluginCode . '/resources/views';
if (File::exists($viewsPath)) {
View::addNamespace($pluginCode, $viewsPath);
}
// 初始化插件
if (method_exists($plugin, 'boot')) {
$plugin->boot();
}
return true; return true;
} }
@ -169,32 +357,6 @@ class PluginManager
return true; return true;
} }
/**
* 加载插件实例
*/
protected function loadPlugin(string $pluginCode)
{
$pluginFile = $this->pluginPath . '/' . $pluginCode . '/Plugin.php';
if (!File::exists($pluginFile)) {
return null;
}
require_once $pluginFile;
$className = "Plugin\\{$pluginCode}\\Plugin";
return new $className($pluginCode);
}
/**
* 验证配置文件
*/
protected function validateConfig(array $config): bool
{
return isset($config['code'])
&& isset($config['version'])
&& isset($config['description'])
&& isset($config['author']);
}
/** /**
* 检查依赖关系 * 检查依赖关系
*/ */