From ebd5c09145e260fb42de8d02adfb86f420319a0b Mon Sep 17 00:00:00 2001 From: xboard Date: Sun, 9 Feb 2025 22:38:09 +0800 Subject: [PATCH] chore(plugin): Refactor plugin architecture and improve code quality --- .../Controllers/V2/Admin/PluginController.php | 7 +- app/Services/Plugin/AbstractPlugin.php | 109 ++++++- app/Services/Plugin/PluginConfigService.php | 11 +- app/Services/Plugin/PluginManager.php | 296 ++++++++++++++---- 4 files changed, 350 insertions(+), 73 deletions(-) diff --git a/app/Http/Controllers/V2/Admin/PluginController.php b/app/Http/Controllers/V2/Admin/PluginController.php index 8fcc6f8..05dff13 100644 --- a/app/Http/Controllers/V2/Admin/PluginController.php +++ b/app/Http/Controllers/V2/Admin/PluginController.php @@ -40,9 +40,10 @@ class PluginController extends Controller $configFile = $directory . '/config.json'; if (File::exists($configFile)) { $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[] = [ 'code' => $config['code'], 'name' => $config['name'], @@ -50,7 +51,7 @@ class PluginController extends Controller 'description' => $config['description'], 'author' => $config['author'], 'is_installed' => $installed, - 'is_enabled' => $installed ? $installedPlugins[$pluginName]['is_enabled'] : false, + 'is_enabled' => $installed ? $installedPlugins[$code]['is_enabled'] : false, 'config' => $pluginConfig, ]; } diff --git a/app/Services/Plugin/AbstractPlugin.php b/app/Services/Plugin/AbstractPlugin.php index 0da4f2e..2c0d9cc 100644 --- a/app/Services/Plugin/AbstractPlugin.php +++ b/app/Services/Plugin/AbstractPlugin.php @@ -5,6 +5,7 @@ namespace App\Services\Plugin; use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\File; +use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\Response; abstract class AbstractPlugin @@ -12,14 +13,40 @@ abstract class AbstractPlugin protected array $config = []; protected string $basePath; protected string $pluginCode; + protected string $namespace; - public function __construct($pluginCode) + public function __construct(string $pluginCode) { $this->pluginCode = $pluginCode; + $this->namespace = 'Plugin\\' . Str::studly($pluginCode); $reflection = new \ReflectionClass($this); $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; } + /** + * 获取视图 + */ + 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); } + + /** + * 插件启动时调用 + */ + 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'; + } } \ No newline at end of file diff --git a/app/Services/Plugin/PluginConfigService.php b/app/Services/Plugin/PluginConfigService.php index 6fc0914..c9b323d 100644 --- a/app/Services/Plugin/PluginConfigService.php +++ b/app/Services/Plugin/PluginConfigService.php @@ -7,6 +7,13 @@ use Illuminate\Support\Facades\File; class PluginConfigService { + protected $pluginManager; + + public function __construct() + { + $this->pluginManager = app(PluginManager::class); + } + /** * 获取插件配置 * @@ -73,7 +80,7 @@ class PluginConfigService */ protected function getDefaultConfig(string $pluginCode): array { - $configFile = base_path("plugins/{$pluginCode}/config.json"); + $configFile = $this->pluginManager->getPluginPath($pluginCode) . '/config.json'; if (!File::exists($configFile)) { return []; } @@ -88,7 +95,7 @@ class PluginConfigService * @param string $pluginCode * @return array */ - protected function getDbConfig(string $pluginCode): array + public function getDbConfig(string $pluginCode): array { $plugin = Plugin::query() ->where('code', $pluginCode) diff --git a/app/Services/Plugin/PluginManager.php b/app/Services/Plugin/PluginManager.php index c1ac99a..863f7ea 100644 --- a/app/Services/Plugin/PluginManager.php +++ b/app/Services/Plugin/PluginManager.php @@ -6,54 +6,248 @@ use App\Models\Plugin; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\File; 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 { protected string $pluginPath; + protected array $loadedPlugins = []; public function __construct() { $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 { - $configFile = $this->pluginPath . '/' . $pluginCode . '/config.json'; + DB::beginTransaction(); + try { + $configFile = $this->getPluginPath($pluginCode) . '/config.json'; - if (!File::exists($configFile)) { - throw new \Exception('Plugin config file not found'); + if (!File::exists($configFile)) { + throw new \Exception('Plugin config file not found'); + } + + $config = json_decode(File::get($configFile), true); + if (!$this->validateConfig($config)) { + throw new \Exception('Invalid plugin config'); + } + + // 检查插件是否已安装 + if (Plugin::where('code', $pluginCode)->exists()) { + throw new \Exception('Plugin already installed'); + } + + // 检查依赖 + if (!$this->checkDependencies($config['require'] ?? [])) { + throw new \Exception('Dependencies not satisfied'); + } + + // 运行数据库迁移 + $this->runMigrations($pluginCode); + + // 提取配置默认值 + $defaultValues = $this->extractDefaultConfig($config); + + // 创建插件实例 + $plugin = $this->loadPlugin($pluginCode); + + // 注册到数据库 + $dbPlugin = Plugin::create([ + 'code' => $pluginCode, + 'name' => $config['name'], + 'version' => $config['version'], + 'is_enabled' => false, + 'config' => json_encode($defaultValues), + 'installed_at' => now(), + ]); + + // 运行插件安装方法 + if (method_exists($plugin, 'install')) { + $plugin->install(); + } + + // 发布插件资源 + $this->publishAssets($pluginCode); + + DB::commit(); + return true; + } catch (\Exception $e) { + DB::rollBack(); + throw $e; } + } - $config = json_decode(File::get($configFile), true); - if (!$this->validateConfig($config)) { - throw new \Exception('Invalid plugin config'); - } - - // 检查依赖 - if (!$this->checkDependencies($config['require'] ?? [])) { - throw new \Exception('Dependencies not satisfied'); - } - - // 提取配置默认值 + /** + * 提取插件默认配置 + */ + protected function extractDefaultConfig(array $config): array + { $defaultValues = []; if (isset($config['config']) && is_array($config['config'])) { foreach ($config['config'] as $key => $item) { - $defaultValues[$key] = $item['default'] ?? null; + 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; } } - // 注册到数据库 - Plugin::create([ - 'code' => $pluginCode, - 'name' => $config['name'], - 'version' => $config['version'], - 'is_enabled' => false, - 'config' => json_encode($defaultValues), - 'installed_at' => now(), - ]); + // 验证插件代码格式 + if (!preg_match('/^[a-z0-9_]+$/', $config['code'])) { + return false; + } + + // 验证版本号格式 + if (!preg_match('/^\d+\.\d+\.\d+$/', $config['version'])) { + return false; + } return true; } @@ -64,8 +258,9 @@ class PluginManager public function enable(string $pluginCode): bool { $plugin = $this->loadPlugin($pluginCode); + 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)); } + // 注册服务提供者 + $this->registerServiceProvider($pluginCode); + + // 加载路由 + $this->loadRoutes($pluginCode); + + // 加载视图 + $this->loadViews($pluginCode); + // 更新数据库状态 Plugin::query() ->where('code', $pluginCode) @@ -85,22 +289,6 @@ class PluginManager '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; } @@ -169,32 +357,6 @@ class PluginManager 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']); - } - /** * 检查依赖关系 */