diff --git a/.vitepress/.gitignore b/.vitepress/.gitignore index afee1cf..d5b1077 100644 --- a/.vitepress/.gitignore +++ b/.vitepress/.gitignore @@ -1,3 +1,4 @@ .temp cache -.vite \ No newline at end of file +.vite +dist \ No newline at end of file diff --git a/.vitepress/components/code-group.vue b/.vitepress/components/code-group.vue index b603603..58794fa 100644 --- a/.vitepress/components/code-group.vue +++ b/.vitepress/components/code-group.vue @@ -31,11 +31,228 @@ const currentTab = ref('index.vue'); \ No newline at end of file +
+ + +
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/.vitepress/components/demo-preview.vue b/.vitepress/components/demo-preview.vue index b7455d8..646cc3b 100644 --- a/.vitepress/components/demo-preview.vue +++ b/.vitepress/components/demo-preview.vue @@ -1,13 +1,17 @@ \ No newline at end of file + + + \ No newline at end of file diff --git a/.vitepress/config.mts b/.vitepress/config.mts index c766d7d..46f5561 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -70,7 +70,34 @@ const viteConfig:ViteConfig = { filename: 'stats.html', open: true }) - ] + ], + build: { + // 修复生产环境样式丢失问题 + cssCodeSplit: false, + rollupOptions: { + output: { + assetFileNames: (assetInfo) => { + // 确保 CSS 文件使用相对路径 + const info = assetInfo.name?.split('.') ?? [] + const extType = info[info.length - 1] + if (/\.(css)$/i.test(assetInfo.name ?? '')) { + return `assets/[name]-[hash][extname]` + } + return `assets/[name]-[hash][extname]` + } + } + } + }, + css: { + postcss: { + plugins: [] + } + }, + define: { + // 确保生产环境变量正确设置 + __VUE_OPTIONS_API__: true, + __VUE_PROD_DEVTOOLS__: false + } } const config:UserConfig = { @@ -79,6 +106,15 @@ const config:UserConfig = { lang: currentLanguage, ...langConfig, srcDir: `docs/${currentLanguage}`, + head: [ + ['meta', { name: 'algolia-site-verification', content: '28481EEA8E61E1C8' }], + // Element Plus CDN CSS - 使用多个备用源确保可靠性 + ['link', { rel: 'stylesheet', href: 'https://unpkg.com/element-plus@2.10.6/dist/index.css' }], + ['link', { rel: 'stylesheet', href: 'https://unpkg.com/element-plus@2.10.6/theme-chalk/dark/css-vars.css' }], + // 备用CDN源 + ['link', { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/element-plus@2.10.6/dist/index.css', media: 'print', onload: "this.media='all'" }], + ['noscript', {}, ''] + ], themeConfig: { logo: '/logo.svg', outline:{ @@ -95,7 +131,6 @@ const config:UserConfig = { ], ...langConfig.themeConfig }, - assetsDir:'/static', markdown:{ lineNumbers: true, languages: [ diff --git a/.vitepress/dist/.gitignore b/.vitepress/dist/.gitignore deleted file mode 100644 index 05f7297..0000000 --- a/.vitepress/dist/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -assets -backend -faq.md -backend.md -plugin.md -llms-full.txt -llms.txt -guide -front -plugin \ No newline at end of file diff --git a/.vitepress/dist/static/images/create_app.png b/.vitepress/dist/static/images/create_app.png deleted file mode 100644 index 09cb2ca..0000000 Binary files a/.vitepress/dist/static/images/create_app.png and /dev/null differ diff --git a/.vitepress/dist/static/images/gitee.svg b/.vitepress/dist/static/images/gitee.svg deleted file mode 100644 index aa57859..0000000 --- a/.vitepress/dist/static/images/gitee.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/.vitepress/dist/static/logo.svg b/.vitepress/dist/static/logo.svg deleted file mode 100644 index c87f35c..0000000 --- a/.vitepress/dist/static/logo.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.vitepress/src/en/sidebars.ts b/.vitepress/src/en/sidebars.ts index 4f0a6ee..5bab5ed 100644 --- a/.vitepress/src/en/sidebars.ts +++ b/.vitepress/src/en/sidebars.ts @@ -19,7 +19,7 @@ const sidebar:DefaultTheme.Sidebar = { link: '/guide/introduce/declaration', }, { - text: '🙏 Acknowledgments', + text: '🙏 Acknowledgements', link: '/guide/introduce/thank', } ] @@ -39,7 +39,7 @@ const sidebar:DefaultTheme.Sidebar = { ] }, { - text: '📋 Additional Resources', + text: '📋 Other Resources', collapsed: true, items: [ { @@ -59,7 +59,7 @@ const sidebar:DefaultTheme.Sidebar = { ], '/front/': [ { - text: '🎯 Getting Started', + text: '🎯 Basics', collapsed: false, items: [ { @@ -71,7 +71,7 @@ const sidebar:DefaultTheme.Sidebar = { link: '/front/base/start' }, { - text: '🧭 Routes & Menus', + text: '🧭 Routing & Menus', link: '/front/base/route-menu' }, { @@ -253,7 +253,7 @@ const sidebar:DefaultTheme.Sidebar = { link: '/front/component/ma-table/tree-table' }, { - text: '☑️ Multi-select Table', + text: '☑️ Multi-Select', link: '/front/component/ma-table/selection' }, { @@ -360,7 +360,7 @@ const sidebar:DefaultTheme.Sidebar = { text: "🔄 Lifecycle", link: "/backend/base/lifecycle" }, - { text: "🌐 Routes & API Docs",link: "/backend/base/router"}, + { text: "🌐 Routing & API Docs",link: "/backend/base/router"}, { text: "⚠️ Error Handling",link: "/backend/base/error-handler"}, {text: "📄 Logging",link: "/backend/base/logger"}, {text: "📡 Events",link: "/backend/base/event-handler"}, @@ -381,7 +381,7 @@ const sidebar:DefaultTheme.Sidebar = { link: "/backend/security/access" }, { - text: "🌍 Client IP", + text: "🌍 Client IP Detection", link: "/backend/security/client-ip" } ] @@ -423,29 +423,63 @@ const sidebar:DefaultTheme.Sidebar = { items: [ { - text:"📝 Preparation", - link:"/plugin" + text:"📖 Plugin System Overview", + link:"/plugin/index" + }, + { + text:"🎯 Quick Start Guide", + link:"/plugin/guide" }, { text:"💻 Plugin Commands", link:"/plugin/command" }, { - text:"✨ Create App", + text:"✨ Create Application", link:"/plugin/create" - }, + } + ] + }, + { + text:"📚 Core Concepts", + collapsed: false, + items: + [ { text:"📁 Plugin Structure", link:"/plugin/structure" }, { - text:"📄 mine.json Documentation", + text:"📄 mine.json Configuration", link:"/plugin/mineJson" }, { - text:"⚙️ ConfigProvider Guide", - link:"/plugin/configProvider" - } + text:"⚙️ ConfigProvider Guide", + link:"/plugin/configProvider" + }, + { + text:"🔄 Lifecycle Management", + link:"/plugin/lifecycle" + } + ] + }, + { + text:"💡 Development Guide", + collapsed: false, + items: + [ + { + text:"🛠️ Plugin Development Guide", + link:"/plugin/develop" + }, + { + text:"📚 API Reference", + link:"/plugin/api" + }, + { + text:"📝 Example Code", + link:"/plugin/examples" + } ] }, { @@ -467,7 +501,7 @@ const sidebar:DefaultTheme.Sidebar = { collapsed: false, items:[ { - text: "📋 Frontend Standards", + text: "📋 Frontend Development Standards", link: "/plugin/front/develop" } ] diff --git a/.vitepress/src/ja/sidebars.ts b/.vitepress/src/ja/sidebars.ts index 67ebeb7..e0eca63 100644 --- a/.vitepress/src/ja/sidebars.ts +++ b/.vitepress/src/ja/sidebars.ts @@ -7,7 +7,7 @@ const sidebar:DefaultTheme.Sidebar = { collapsed: false, items: [ { - text: '💡 なぜ私たちを選ぶのか?', + text: '💡 なぜ私たちを選ぶのか?', link: '/guide/introduce/mineadmin', }, { @@ -51,7 +51,7 @@ const sidebar:DefaultTheme.Sidebar = { link:"/guide/upgrade" }, { - text:"🤝 貢献ガイド", + text:"🤝 コントリビューションガイド", link:"/guide/contributions" } ] @@ -131,7 +131,7 @@ const sidebar:DefaultTheme.Sidebar = { ] }, { - text: '🎪 高度なトピック', + text: '🎪 高度なテーマ', collapsed: true, items: [ { @@ -361,7 +361,7 @@ const sidebar:DefaultTheme.Sidebar = { link: "/backend/base/lifecycle" }, { text: "🌐 ルートとAPIドキュメント",link: "/backend/base/router"}, - { text: "⚠️ エラー処理",link: "/backend/base/error-handler"}, + { text: "⚠️ エラーハンドリング",link: "/backend/base/error-handler"}, {text: "📄 ログ",link: "/backend/base/logger"}, {text: "📡 イベント",link: "/backend/base/event-handler"}, {text: "📄 ファイルアップロード",link: "/backend/base/upload"}, @@ -423,8 +423,12 @@ const sidebar:DefaultTheme.Sidebar = { items: [ { - text:"📝 準備作業", - link:"/plugin" + text:"📖 プラグインシステム概要", + link:"/plugin/index" + }, + { + text:"🎯 クイックスタートガイド", + link:"/plugin/guide" }, { text:"💻 プラグインコマンド", @@ -433,19 +437,49 @@ const sidebar:DefaultTheme.Sidebar = { { text:"✨ アプリケーション作成", link:"/plugin/create" - }, + } + ] + }, + { + text:"📚 コアコンセプト", + collapsed: false, + items: + [ { text:"📁 プラグインディレクトリ構造", link:"/plugin/structure" }, { - text:"📄 mine.json 説明と例", + text:"📄 mine.json 設定", link:"/plugin/mineJson" }, { - text:"⚙️ ConfigProvider 説明", - link:"/plugin/configProvider" - } + text:"⚙️ ConfigProvider 説明", + link:"/plugin/configProvider" + }, + { + text:"🔄 ライフサイクル管理", + link:"/plugin/lifecycle" + } + ] + }, + { + text:"💡 開発ガイド", + collapsed: false, + items: + [ + { + text:"🛠️ プラグイン開発ガイド", + link:"/plugin/develop" + }, + { + text:"📚 API リファレンスドキュメント", + link:"/plugin/api" + }, + { + text:"📝 サンプルコード", + link:"/plugin/examples" + } ] }, { diff --git a/.vitepress/src/zh-hk/sidebars.ts b/.vitepress/src/zh-hk/sidebars.ts index 048e79b..da0ec35 100644 --- a/.vitepress/src/zh-hk/sidebars.ts +++ b/.vitepress/src/zh-hk/sidebars.ts @@ -423,8 +423,12 @@ const sidebar:DefaultTheme.Sidebar = { items: [ { - text:"📝 準備工作", - link:"/plugin" + text:"📖 插件系統概述", + link:"/plugin/index" + }, + { + text:"🎯 快速入門指南", + link:"/plugin/guide" }, { text:"💻 插件命令", @@ -433,19 +437,49 @@ const sidebar:DefaultTheme.Sidebar = { { text:"✨ 創建應用", link:"/plugin/create" - }, + } + ] + }, + { + text:"📚 核心概念", + collapsed: false, + items: + [ { text:"📁 插件目錄結構", link:"/plugin/structure" }, { - text:"📄 mine.json 説明及示例", + text:"📄 mine.json 配置", link:"/plugin/mineJson" }, { - text:"⚙️ ConfigProvider 説明", - link:"/plugin/configProvider" - } + text:"⚙️ ConfigProvider 説明", + link:"/plugin/configProvider" + }, + { + text:"🔄 生命週期管理", + link:"/plugin/lifecycle" + } + ] + }, + { + text:"💡 開發指南", + collapsed: false, + items: + [ + { + text:"🛠️ 插件開發指南", + link:"/plugin/develop" + }, + { + text:"📚 API 參考文檔", + link:"/plugin/api" + }, + { + text:"📝 示例代碼", + link:"/plugin/examples" + } ] }, { diff --git a/.vitepress/src/zh-tw/sidebars.ts b/.vitepress/src/zh-tw/sidebars.ts index 51a7263..2f7ff56 100644 --- a/.vitepress/src/zh-tw/sidebars.ts +++ b/.vitepress/src/zh-tw/sidebars.ts @@ -423,8 +423,12 @@ const sidebar:DefaultTheme.Sidebar = { items: [ { - text:"📝 準備工作", - link:"/plugin" + text:"📖 外掛系統概述", + link:"/plugin/index" + }, + { + text:"🎯 快速入門指南", + link:"/plugin/guide" }, { text:"💻 外掛命令", @@ -433,19 +437,49 @@ const sidebar:DefaultTheme.Sidebar = { { text:"✨ 建立應用", link:"/plugin/create" - }, + } + ] + }, + { + text:"📚 核心概念", + collapsed: false, + items: + [ { text:"📁 外掛目錄結構", link:"/plugin/structure" }, { - text:"📄 mine.json 說明及示例", + text:"📄 mine.json 配置", link:"/plugin/mineJson" }, { - text:"⚙️ ConfigProvider 說明", - link:"/plugin/configProvider" - } + text:"⚙️ ConfigProvider 說明", + link:"/plugin/configProvider" + }, + { + text:"🔄 生命週期管理", + link:"/plugin/lifecycle" + } + ] + }, + { + text:"💡 開發指南", + collapsed: false, + items: + [ + { + text:"🛠️ 外掛開發指南", + link:"/plugin/develop" + }, + { + text:"📚 API 參考文件", + link:"/plugin/api" + }, + { + text:"📝 示例程式碼", + link:"/plugin/examples" + } ] }, { diff --git a/.vitepress/src/zh/sidebars.ts b/.vitepress/src/zh/sidebars.ts index 821e126..90a7847 100644 --- a/.vitepress/src/zh/sidebars.ts +++ b/.vitepress/src/zh/sidebars.ts @@ -423,8 +423,12 @@ const sidebar:DefaultTheme.Sidebar = { items: [ { - text:"📝 准备工作", - link:"/plugin" + text:"📖 插件系统概述", + link:"/plugin/index" + }, + { + text:"🎯 快速入门指南", + link:"/plugin/guide" }, { text:"💻 插件命令", @@ -433,19 +437,49 @@ const sidebar:DefaultTheme.Sidebar = { { text:"✨ 创建应用", link:"/plugin/create" - }, + } + ] + }, + { + text:"📚 核心概念", + collapsed: false, + items: + [ { text:"📁 插件目录结构", link:"/plugin/structure" }, { - text:"📄 mine.json 说明及示例", + text:"📄 mine.json 配置", link:"/plugin/mineJson" }, { - text:"⚙️ ConfigProvider 说明", - link:"/plugin/configProvider" - } + text:"⚙️ ConfigProvider 说明", + link:"/plugin/configProvider" + }, + { + text:"🔄 生命周期管理", + link:"/plugin/lifecycle" + } + ] + }, + { + text:"💡 开发指南", + collapsed: false, + items: + [ + { + text:"🛠️ 插件开发指南", + link:"/plugin/develop" + }, + { + text:"📚 API 参考文档", + link:"/plugin/api" + }, + { + text:"📝 示例代码", + link:"/plugin/examples" + } ] }, { diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index a49ffbe..1a66086 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -51,8 +51,6 @@ import enhanceTOC from "./enhanceTOC"; import "virtual:uno.css"; import DemoPreview from '../components/demo-preview.vue'; -import 'element-plus/dist/index.css' -import 'element-plus/theme-chalk/dark/css-vars.css' import CopyOrDownloadAsMarkdownButtons from 'vitepress-plugin-llms/vitepress-components/CopyOrDownloadAsMarkdownButtons.vue' diff --git a/docs/en/plugin/api.md b/docs/en/plugin/api.md new file mode 100644 index 0000000..2d6fcb9 --- /dev/null +++ b/docs/en/plugin/api.md @@ -0,0 +1,745 @@ +# API Reference Documentation + +This document provides detailed information about all API interfaces, command-line tools, and core libraries of the MineAdmin plugin system. + +## Command Line API + +### Plugin Management Commands + +#### 1. mine-extension:initial + +Initialize the plugin extension system. + +```bash +php bin/hyperf.php mine-extension:initial +``` + +**Features**: +- Publish app-store configuration files +- Initialize plugin system configuration +- Create necessary directory structure + +**Implementation Class**: `Mine\AppStore\Command\InitialCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/InitialCommand.php)) + +#### 2. mine-extension:list + +Query remote plugin list. + +```bash +php bin/hyperf.php mine-extension:list [options] +``` + +**Parameters**: +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| --type | string | all | Filter extension type (mixed/backend/frontend) | +| --name | string | - | Filter by extension name | +| --category | string | - | Filter by category | +| --author | string | - | Filter by author | + +**Examples**: +```bash +# View all plugins +php bin/hyperf.php mine-extension:list + +# View mixed-type plugins +php bin/hyperf.php mine-extension:list --type=mixed + +# Search for specific plugin +php bin/hyperf.php mine-extension:list --name=user-manager +``` + +**Implementation Class**: `Mine\AppStore\Command\ListCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/ListCommand.php)) + +#### 3. mine-extension:local-list + +Query all local plugins. + +```bash +php bin/hyperf.php mine-extension:local-list [options] +``` + +**Parameters**: +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| --status | string | all | Filter by status (installed/enabled/disabled) | +| --type | string | all | Filter by type | + +**Implementation Class**: `Mine\AppStore\Command\LocalListCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/LocalListCommand.php)) + +#### 4. mine-extension:download + +Download remote plugin to local. + +```bash +php bin/hyperf.php mine-extension:download --name=plugin-name [options] +``` + +**Parameters**: +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| --name | string | Yes | Plugin name | +| --version | string | No | Specify version | +| --force | bool | No | Force overwrite existing plugin | + +**Implementation Class**: `Mine\AppStore\Command\DownloadCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/DownloadCommand.php)) + +#### 5. mine-extension:install + +Install specified plugin. + +```bash +php bin/hyperf.php mine-extension:install {path} [options] +``` + +**Parameters**: +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| path | string | Yes | Plugin path (vendor/plugin-name) | +| --yes | bool | No | Skip confirmation prompt | +| --force | bool | No | Force reinstall | +| --skip-dependencies | bool | No | Skip dependency check | + +**Examples**: +```bash +# Install plugin +php bin/hyperf.php mine-extension:install mineadmin/user-manager --yes + +# Force reinstall +php bin/hyperf.php mine-extension:install mineadmin/user-manager --force +``` + +**Implementation Class**: `Mine\AppStore\Command\InstallCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/InstallCommand.php)) + +#### 6. mine-extension:uninstall + +Uninstall specified plugin. + +```bash +php bin/hyperf.php mine-extension:uninstall {path} [options] +``` + +**Parameters**: +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| path | string | Yes | Plugin path | +| --yes | bool | No | Skip confirmation prompt | +| --force | bool | No | Force uninstall (ignore errors) | +| --keep-data | bool | No | Preserve user data | + +**Implementation Class**: `Mine\AppStore\Command\UninstallCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/UninstallCommand.php)) + +#### 7. mine-extension:create + +Create new plugin. + +```bash +php bin/hyperf.php mine-extension:create {path} [options] +``` + +**Parameters**: +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| path | string | - | Plugin path (vendor/plugin-name) | +| --name | string | example | Plugin display name | +| --type | string | mixed | Plugin type (mixed/backend/frontend) | +| --author | string | - | Author name | +| --description | string | - | Plugin description | +| --license | string | MIT | License type | + +**Example**: +```bash +php bin/hyperf.php mine-extension:create mycompany/hello-world \ + --name "Hello World" \ + --type mixed \ + --author "Your Name" \ + --description "My first plugin" +``` + +**Implementation Class**: `Mine\AppStore\Command\CreateCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/CreateCommand.php)) + +## Core Library API + +### Plugin Class + +**File Location**: `Mine\AppStore\Plugin` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Plugin.php)) + +Core class of the plugin system, responsible for plugin loading and management. + +#### Plugin::init() + +Initialize the plugin system, called when the application starts. + +```php + [ + 'name' => 'vendor/plugin-name', + 'version' => '1.0.0', + 'path' => '/path/to/plugin', + 'config' => [...], // mine.json configuration + 'status' => 'enabled' + ] +] +``` + +#### Plugin::isInstalled() + +Check if plugin is installed. + +```php +install('vendor/plugin-name', [ + 'force' => false, + 'skip_dependencies' => false +]); + +if ($result['success']) { + echo "Installation successful"; +} else { + echo "Installation failed: " . $result['message']; +} +``` + +#### uninstall() + +Uninstall plugin. + +```php +uninstall('vendor/plugin-name', [ + 'force' => false, + 'keep_data' => false +]); +``` + +#### update() + +Update plugin. + +```php +update('vendor/plugin-name'); +``` + +### ConfigProvider Base Class + +All plugin ConfigProviders should follow this interface: + +```php + [ + InterfaceA::class => ImplementationA::class, + ], + + // Annotation scan paths + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + + // Command line commands + 'commands' => [ + CustomCommand::class, + ], + + // Event listeners + 'listeners' => [ + CustomListener::class, + ], + + // Middleware + 'middlewares' => [ + 'http' => [ + CustomMiddleware::class, + ], + ], + + // Configuration file publishing + 'publish' => [ + [ + 'id' => 'config-id', + 'description' => 'Configuration file description', + 'source' => __DIR__ . '/../publish/config.php', + 'destination' => BASE_PATH . '/config/autoload/plugin.php', + ], + ], + + // Process configuration + 'processes' => [ + CustomProcess::class, + ], + ]; + } +} +``` + +## HTTP API + +### Plugin Management Interface + +#### Get Plugin List + +```http +GET /admin/plugin/list +``` + +**Request Parameters**: +```json +{ + "page": 1, + "pageSize": 15, + "type": "mixed", + "status": "enabled", + "keyword": "search term" +} +``` + +**Response Example**: +```json +{ + "code": 200, + "message": "success", + "data": { + "list": [ + { + "name": "vendor/plugin-name", + "display_name": "Plugin Display Name", + "version": "1.0.0", + "description": "Plugin description", + "author": "Author name", + "type": "mixed", + "status": "enabled", + "installed_at": "2024-01-01 12:00:00", + "updated_at": "2024-01-15 10:30:00" + } + ], + "total": 1 + } +} +``` + +#### Install Plugin + +```http +POST /admin/plugin/install +``` + +**Request Parameters**: +```json +{ + "name": "vendor/plugin-name", + "version": "1.0.0", + "force": false +} +``` + +**Response Example**: +```json +{ + "code": 200, + "message": "Installation successful", + "data": { + "plugin": "vendor/plugin-name", + "version": "1.0.0", + "installed_at": "2024-01-01 12:00:00" + } +} +``` + +#### Uninstall Plugin + +```http +DELETE /admin/plugin/uninstall +``` + +**Request Parameters**: +```json +{ + "name": "vendor/plugin-name", + "keep_data": false +} +``` + +#### Enable/Disable Plugin + +```http +PUT /admin/plugin/toggle-status +``` + +**Request Parameters**: +```json +{ + "name": "vendor/plugin-name", + "status": "enabled" // enabled | disabled +} +``` + +## Event API + +### Plugin Event System + +The plugin system provides rich event hooks, allowing developers to execute custom logic at key points in the plugin lifecycle. + +#### Event Types + +```php +success) { + // Post-installation processing + $this->clearCache(); + $this->sendNotification($event->pluginName); + $this->updateStatistics($event->pluginName); + } else { + // Installation failure handling + logger()->error('Plugin installation failed', [ + 'plugin' => $event->pluginName, + 'error' => $event->error + ]); + } + } +} +``` + +## Hook API + +### Plugin Hook System + +MineAdmin provides a hook system that allows plugins to inject custom logic at key system points. + +#### Register Hook + +```php +info('User attempting login', ['user_id' => $user->id]); + }); + + HookManager::register('user.login.after', function($user) { + // Post-login processing logic + $this->recordLoginHistory($user); + }); + + return [ + // ... other configurations + ]; + } +} +``` + +#### Trigger Hook + +```php +authenticate($credentials); + + if ($result) { + // Post-login hook + HookManager::trigger('user.login.after', $user); + } + + return $result; + } +} +``` + +#### Available Hooks List + +| Hook Name | Trigger Point | Parameters | +|-----------|---------------|------------| +| `user.login.before` | Before user login | User $user | +| `user.login.after` | After user login | User $user | +| `user.logout.before` | Before user logout | User $user | +| `user.logout.after` | After user logout | User $user | +| `menu.render.before` | Before menu rendering | array $menus | +| `menu.render.after` | After menu rendering | array $menus | +| `permission.check.before` | Before permission check | string $permission, User $user | +| `permission.check.after` | After permission check | bool $result, string $permission, User $user | + +## Utility Class API + +### PluginHelper Class + +Provides common utility methods for plugin development. + +```php + config +config --> provider +provider --> backend +provider --> frontend +backend --> script +frontend --> script +script --> test +test --> publish + +note right of create : mine-extension:create +note right of config : Plugin metadata configuration +note right of provider : Register services and routes +note right of backend : Controller + Service +note right of frontend : Vue3 + TypeScript +note right of script : InstallScript/UninstallScript +note right of test : Local installation testing +note right of publish : Publish to marketplace + +@enduml +``` + +## Plugin Structure Standards + +Based on actual code from `app-store` and `code-generator` plugins, MineAdmin plugins have two typical structures: + +### Simple Plugin Structure (Suitable for backend-only or simple features) + +``` +plugin/mine-admin/plugin-name/ +├── mine.json # Plugin config file +├── install.lock # Installation marker (auto-generated) +└── src/ + ├── ConfigProvider.php # Configuration provider + ├── Controller/ # Controllers + │ └── IndexController.php + └── Service/ # Service layer + └── Service.php +``` + +### Complete Plugin Structure (Suitable for complex business logic) + +``` +plugin/mine-admin/plugin-name/ +├── mine.json # Plugin config file +├── install.lock # Installation marker (auto-generated) +├── README.md # Plugin documentation +├── src/ # Backend code +│ ├── ConfigProvider.php # Configuration provider +│ ├── InstallScript.php # Installation script +│ ├── UninstallScript.php # Uninstallation script +│ ├── Http/ +│ │ ├── Controller/ # Controllers +│ │ ├── Request/ # Request validation +│ │ └── Vo/ # Value objects +│ ├── Model/ # Data models +│ ├── Repository/ # Repository layer +│ └── Service/ # Service layer +├── web/ # Frontend code +│ ├── index.ts # Plugin entry +│ ├── api/ # API interfaces +│ ├── views/ # Vue components +│ └── locales/ # Language packs +├── Database/ # Database +│ ├── Migrations/ # Migration files +│ └── Seeder/ # Seed data +├── languages/ # Backend language packs +│ └── zh_CN/ +└── publish/ # Published resources + └── template/ # Template files +``` + +## Backend Development + +### 1. ConfigProvider Configuration Provider + +Based on actual implementation from app-store plugin: + +```php + [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + // Dependency injection (optional) + 'dependencies' => [ + // Interface::class => Implementation::class + ], + // Commands (optional) + 'commands' => [ + // Command::class + ], + // Middleware (optional) + 'middlewares' => [ + 'http' => [ + // Middleware::class + ], + ], + // Event listeners (optional) + 'listeners' => [ + // Listener::class + ], + ]; + } +} +``` + +### 2. Controller Development + +Reference app-store's IndexController implementation: + +```php +success( + $this->service->getAppList($this->request->all()) + ); + } + + /** + * Download plugin + */ + #[PostMapping("download")] + #[Permission("plugin:store:download")] + public function download(): ResponseInterface + { + $params = $this->request->all(); + $this->service->download($params); + return $this->success(); + } + + /** + * Install plugin + */ + #[PostMapping("install")] + #[Permission("plugin:store:install")] + public function install(): ResponseInterface + { + $params = $this->request->all(); + $this->service->install($params); + return $this->success(); + } + + /** + * Uninstall plugin + */ + #[PostMapping("unInstall")] + #[Permission("plugin:store:uninstall")] + public function unInstall(): ResponseInterface + { + $params = $this->request->all(); + $this->service->unInstall($params); + return $this->success(); + } + + /** + * Local plugin installation list + */ + #[GetMapping("getInstallList")] + #[RemoteState] + public function getInstallList(): ResponseInterface + { + return $this->success( + $this->service->getLocalAppInstallList() + ); + } + + /** + * Local upload installation + */ + #[PostMapping("uploadInstall")] + #[Permission("plugin:store:uploadInstall")] + public function uploadInstall(): ResponseInterface + { + return $this->success( + $this->service->uploadLocalApp($this->request->all()) + ); + } +} +``` + +**Key Annotation Explanations**: +- `#[Controller]`: Defines controller route prefix +- `#[Auth]`: Requires login authentication +- `#[Permission]`: Permission verification +- `#[GetMapping]`/`#[PostMapping]`: Defines route methods +- `#[Inject]`: Dependency injection +- `#[RemoteState]`: Remote state management + +### 3. Service Layer Development + +Based on app-store's Service implementation pattern: + +```php +service->getAppList($params); + } + + /** + * Download application + */ + public function download(array $params): void + { + $app = $this->service->getAppInfo($params['identifier']); + + if (empty($app['download_url'])) { + throw new MineException('This application cannot be downloaded', 500); + } + + if (Plugin::hasLocalInstalled($params['identifier'])) { + throw new MineException('Application already exists locally. To re-download, please delete the local application first', 500); + } + + $this->service->download($params); + } + + /** + * Install application + */ + public function install(array $params): void + { + $pluginName = $params['name']; + + if (!Plugin::hasLocal($pluginName)) { + throw new MineException('Plugin does not exist', 500); + } + + if (Plugin::hasLocalInstalled($pluginName)) { + throw new MineException('Plugin already installed', 500); + } + + Plugin::forceRefreshJsonPath($pluginName); + Plugin::install($pluginName); + } + + /** + * Uninstall application + */ + public function unInstall(array $params): void + { + $pluginName = $params['name']; + + if (!Plugin::hasLocalInstalled($pluginName)) { + throw new MineException('Plugin not installed', 500); + } + + Plugin::uninstall($pluginName); + } + + /** + * Get locally installed plugin list + */ + public function getLocalAppInstallList(): array + { + $list = []; + $plugins = Plugin::getLocalPlugins(); + + foreach ($plugins as $name => $info) { + $app = ['identifier' => $name]; + $app['name'] = $info['name'] ?? 'Unknown'; + $app['status'] = $info['status'] ?? false; + $app['version'] = $info['version'] ?? '0.0.0'; + $app['description'] = $info['description'] ?? 'No description'; + $app['created_at'] = $info['created_at'] ?? ''; + $list[] = $app; + } + + return $list; + } + + /** + * Local upload installation + */ + public function uploadLocalApp(array $params): void + { + if (empty($params['path'])) { + throw new MineException('Please upload the plugin package', 500); + } + + // Extract and verify plugin package + $zipFile = new \ZipArchive(); + $result = $zipFile->open($params['path']); + + if ($result !== true) { + throw new MineException('Plugin package extraction failed', 500); + } + + // Get plugin info and install + $mineJson = $zipFile->getFromName('mine.json'); + if (!$mineJson) { + throw new MineException('Invalid plugin package format, missing mine.json', 500); + } + + $config = json_decode($mineJson, true); + $pluginName = $config['name'] ?? null; + + if (!$pluginName) { + throw new MineException('Plugin package configuration error', 500); + } + + // Extract to plugin directory + $targetPath = Plugin::getPluginPath($pluginName); + $zipFile->extractTo($targetPath); + $zipFile->close(); + + // Refresh cache and install + Plugin::forceRefreshJsonPath($pluginName); + Plugin::install($pluginName); + } +} +``` + +### 4. Model Layer (If database needed) + +Reference code-generator plugin's model implementation: + +```php + 'boolean', + 'is_list' => 'boolean', + 'is_query' => 'boolean', + 'is_required' => 'boolean', + 'is_sort' => 'boolean', + 'is_edit' => 'boolean', + 'is_readonly' => 'boolean', + ]; +} +``` + +## Frontend Development + +### 1. Plugin Entry File (index.ts) + +Based on app-store's frontend implementation: + +```typescript +import type { App } from 'vue' +import type { Plugin } from '#/global' + +const pluginConfig: Plugin.PluginConfig = { + install(app: App) { + // Vue plugin installation hook + console.log('app-store plugin install') + }, + config: { + enable: true, + info: { + name: 'app-store', + version: '1.0.0', + author: 'MineAdmin Team', + description: 'MineAdmin Marketplace Visualization Plugin' + } + }, + views: [ + { + name: 'plugin:store', + path: '/plugin/store', + meta: { + title: 'app_store.app_store', + i18n: true, + icon: 'material-symbols:app-shortcut', + type: 'M', + hidden: false, + componentPath: '/plugin/mine-admin/app-store/views/index.vue', + componentName: 'plugin:mine-admin:app-store:index', + }, + component: () => import('./views/index.vue'), + } + ], +} + +export default pluginConfig +``` + +### 2. API Interface Encapsulation + +```typescript +// api/app-store.ts +import { request } from '@/utils/request' + +// Get remote plugin list +export const getAppList = (params: any) => { + return request.get('/admin/plugin/store/index', { params }) +} + +// Download plugin +export const downloadApp = (data: any) => { + return request.post('/admin/plugin/store/download', data) +} + +// Install plugin +export const installApp = (data: any) => { + return request.post('/admin/plugin/store/install', data) +} + +// Uninstall plugin +export const uninstallApp = (data: any) => { + return request.post('/admin/plugin/store/unInstall', data) +} + +// Get locally installed plugins +export const getInstalledList = () => { + return request.get('/admin/plugin/store/getInstallList') +} + +// Upload local plugin installation +export const uploadInstall = (data: any) => { + return request.post('/admin/plugin/store/uploadInstall', data) +} +``` + +### 3. Vue Component Development + +```vue + + + + +``` + +### 4. Internationalization Support + +```typescript +// locales/zh_CN.ts +export default { + app_store: { + app_store: 'Marketplace', + app_list: 'Application List', + installed: 'Installed', + install: 'Install', + uninstall: 'Uninstall', + download: 'Download', + upload: 'Upload', + local_upload: 'Local Upload', + upload_tips: 'Please select plugin package file (.zip format)', + } +} +``` + +## Installation and Uninstallation Scripts + +### InstallScript.php + +Based on actual implementation from code-generator plugin: + +```php +output = new ConsoleOutput(); + + try { + $this->info('========================================'); + $this->info('MineAdmin Code Generator Plugin'); + $this->info('========================================'); + $this->info('Starting plugin installation...'); + + // 1. Copy template files + $this->copyTemplates(); + + // 2. Copy language packs + $this->copyLanguages(); + + // 3. Publish dependency resources + $this->publishVendor(); + + // 4. Run database migrations + $this->runMigrations(); + + $this->info('Plugin installed successfully!'); + $this->info('========================================'); + + } catch (\Throwable $e) { + $this->error('Plugin installation failed: ' . $e->getMessage()); + throw $e; + } + } + + /** + * Copy template files + */ + protected function copyTemplates(): void + { + $source = dirname(__DIR__) . '/publish/template'; + $target = BASE_PATH . '/runtime/generate/template'; + + if (!is_dir($target)) { + mkdir($target, 0755, true); + } + + Filesystem::copy($source, $target, false); + $this->info('Template files copied successfully'); + } + + /** + * Copy language packs + */ + protected function copyLanguages(): void + { + $source = dirname(__DIR__) . '/languages'; + $target = BASE_PATH . '/storage/languages'; + + Filesystem::copy($source, $target, false); + $this->info('Language packs copied successfully'); + } + + /** + * Publish dependency resources + */ + protected function publishVendor(): void + { + $app = ApplicationContext::getContainer()->get(ApplicationInterface::class); + $app->setAutoExit(false); + + $input = new ArrayInput([ + 'command' => 'vendor:publish', + 'package' => 'hyperf/translation', + \ No newline at end of file diff --git a/docs/en/plugin/examples.md b/docs/en/plugin/examples.md new file mode 100644 index 0000000..440d39b --- /dev/null +++ b/docs/en/plugin/examples.md @@ -0,0 +1,674 @@ +# Plugin Example Code + +This document provides complete MineAdmin plugin development examples, including practical code samples and best practices for different types of plugins. + +## Official Plugin Examples + +### App-Store Plugin (Mixed Type) + +**Repository**: [mineadmin/appstore](https://github.com/mineadmin/appstore) + +App-Store is MineAdmin's only official default plugin, providing application market management functionality, showcasing a complete implementation of a mixed-type plugin. + +#### Core File Structure +``` +plugin/mine-admin/app-store/ +├── mine.json # Plugin configuration +├── src/ # Backend code +│ ├── ConfigProvider.php # Configuration provider +│ ├── Controller/ # Controllers +│ ├── Service/ # Service layer +│ └── Command/ # Commands +├── web/ # Frontend code +│ ├── views/ # Page components +│ └── api/ # API interfaces +└── Database/ # Database +``` + +#### mine.json Configuration Example +```json +{ + "name": "mine-admin/app-store", + "description": "MineAdmin Application Market Visualization Plugin", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "MineAdmin Team", + "role": "developer" + } + ], + "keywords": ["mineadmin", "app-store", "plugin-management"], + "homepage": "https://github.com/mineadmin/appstore", + "license": "MIT", + "composer": { + "require": { + "hyperf/async-queue": "^3.0" + }, + "psr-4": { + "Plugin\\MineAdmin\\AppStore\\": "src" + }, + "config": "Plugin\\MineAdmin\\AppStore\\ConfigProvider" + } +} +``` + +#### ConfigProvider Implementation +```php + [ + // Dependency injection configuration + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'commands' => [ + Command\AppStoreCommand::class, + ], + 'listeners' => [ + Listener\PluginEventListener::class, + ], + 'publish' => [ + [ + 'id' => 'appstore-config', + 'description' => 'App Store configuration file', + 'source' => __DIR__ . '/../publish/appstore.php', + 'destination' => BASE_PATH . '/config/autoload/appstore.php', + ], + ], + ]; + } +} +``` + +## Complete Plugin Development Examples + +### 1. User Management Plugin (Mixed Type) + +Below is a complete user management plugin example demonstrating how to develop a mixed-type plugin with both frontend and backend. + +#### mine.json Configuration +```json +{ + "name": "mycompany/user-manager", + "description": "User Management Plugin", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "composer": { + "require": { + "hyperf/database": "^3.0", + "hyperf/validation": "^3.0" + }, + "psr-4": { + "Plugin\\MyCompany\\UserManager\\": "src" + }, + "config": "Plugin\\MyCompany\\UserManager\\ConfigProvider" + }, + "package": { + "dependencies": { + "element-plus": "^2.4.0" + } + } +} +``` + +#### Core Controller Implementation +```php +request->all(); + $result = $this->service->getPageList($params); + return $this->success($result); + } + + #[PostMapping('/user')] + public function create(): array + { + $data = $this->request->all(); + $user = $this->service->create($data); + return $this->success($user, 'User created successfully'); + } + + #[PutMapping('/user/{id}')] + public function update(int $id): array + { + $data = $this->request->all(); + $this->service->update($id, $data); + return $this->success(null, 'Update successful'); + } + + #[DeleteMapping('/user/{id}')] + public function delete(int $id): array + { + $this->service->delete($id); + return $this->success(null, 'Delete successful'); + } +} +``` + +#### Service Layer Implementation +```php +where(function($q) use ($params) { + $q->where('username', 'like', "%{$params['keyword']}%") + ->orWhere('email', 'like', "%{$params['keyword']}%"); + }); + } + + $paginator = $query->paginate( + $params['pageSize'] ?? 15, + ['*'], + 'page', + $params['page'] ?? 1 + ); + + return [ + 'items' => $paginator->items(), + 'pageInfo' => [ + 'total' => $paginator->total(), + 'currentPage' => $paginator->currentPage(), + 'totalPage' => $paginator->lastPage() + ] + ]; + } + + public function create(array $data): User + { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + return User::create($data); + } + + public function update(int $id, array $data): bool + { + if (isset($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } + + return User::query()->where('id', $id)->update($data) > 0; + } + + public function delete(int $id): bool + { + return User::destroy($id) > 0; + } +} +``` + +### 2. Backend Plugin Example - API Service Plugin + +Below is an example of a pure backend API service plugin. + +#### Plugin Configuration (mine.json) + +```json +{ + "name": "mycompany/api-service", + "description": "API Service Plugin", + "version": "1.0.0", + "type": "backend", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "keywords": ["api", "service"], + "license": "MIT", + "composer": { + "require": { + "guzzlehttp/guzzle": "^7.0" + }, + "psr-4": { + "Plugin\\MyCompany\\ApiService\\": "src" + }, + "config": "Plugin\\MyCompany\\ApiService\\ConfigProvider" + } +} +``` + +#### ConfigProvider Implementation + +```php + [ + Contract\ApiClientInterface::class => Service\ApiClient::class, + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'commands' => [ + Command\ApiSyncCommand::class, + ], + 'publish' => [ + [ + 'id' => 'api-service-config', + 'description' => 'API Service configuration file', + 'source' => __DIR__ . '/../publish/api_service.php', + 'destination' => BASE_PATH . '/config/autoload/api_service.php', + ], + ], + ]; + } +} +``` + +### 3. Frontend Plugin Example - Data Visualization Plugin + +Below is an example of a pure frontend data visualization plugin. + +#### mine.json Configuration +```json +{ + "name": "mycompany/data-visualization", + "description": "Data Visualization Plugin", + "version": "1.0.0", + "type": "frontend", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "package": { + "dependencies": { + "echarts": "^5.4.0", + "vue-echarts": "^6.5.0" + } + } +} +``` + +## Complete Plugin Development Best Practices + +### 1. Directory Structure Standards + +``` +plugin/vendor-name/plugin-name/ +├── mine.json # Plugin configuration +├── src/ # PHP backend code +│ ├── ConfigProvider.php # Configuration provider +│ ├── Controller/ # Controllers +│ ├── Service/ # Service layer +│ ├── Model/ # Models +│ ├── Command/ # Commands +│ ├── Listener/ # Event listeners +│ └── Middleware/ # Middleware +├── web/ # Frontend code +│ ├── views/ # Vue components +│ ├── api/ # API wrappers +│ ├── components/ # Shared components +│ └── locales/ # Localization +├── Database/ # Database +│ ├── Migrations/ # Migration files +│ └── Seeders/ # Seeders +└── publish/ # Published files + └── config.php # Configuration files +``` + +### 2. Naming Conventions + +- **Plugin Name**: Use `vendor/plugin-name` format +- **Namespace**: `Plugin\VendorName\PluginName` +- **Class Names**: Use PascalCase +- **Method Names**: Use camelCase + +### 3. Core Component Examples + +#### Controller Example + +```php +request->all(); + $result = $this->service->getList($params); + return $this->success($result); + } + + #[PostMapping('/create')] + public function create(): array + { + $data = $this->request->all(); + $result = $this->service->create($data); + return $this->success($result, 'Created successfully'); + } + $user = $this->userService->find($id); + + if (!$user) { + return $this->error('User not found', 404); + } + + return $this->success($user); + } + + /** + * Update user + */ + #[PutMapping('/users/{id:\d+}')] + public function update(int $id): array + { + $data = $this->request->all(); + + $user = $this->userService->update($id, $data); + + return $this->success($user, 'User updated successfully'); + } + + /** + * Delete user + */ + #[DeleteMapping('/users/{id:\d+}')] + public function destroy(int $id): array + { + $this->userService->delete($id); + + return $this->success([], 'User deleted successfully'); + } + + /** + * Batch import users + */ + #[PostMapping('/users/import')] + public function import(): array + { + $file = $this->request->file('file'); + + if (!$file || !$file->isValid()) { + return $this->error('Please upload a valid file'); + } + + $result = $this->userService->importFromFile($file); + + return $this->success($result, 'Import completed'); + } + + /** + * Export user data + */ + #[GetMapping('/users/export')] + public function export(): array + { + $params = $this->request->all(); + $filePath = $this->userService->exportToFile($params); + + return $this->success(['file_path' => $filePath], 'Export successful'); + } +} +``` + +#### 4. Service Layer (src/Service/UserService.php) + +```php +repository->getList($params); + } + + /** + * Create user + */ + public function create(array $data): array + { + // Password encryption + if (isset($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } + + // Generate user avatar + if (!isset($data['avatar'])) { + $data['avatar'] = $this->generateAvatar($data['username']); + } + + $user = $this->repository->create($data); + + // Trigger user creation event + event(new UserCreatedEvent($user)); + + return $user->toArray(); + } + + /** + * Update user + */ + public function update(int $id, array $data): array + { + // Password update handling + if (isset($data['password']) && !empty($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } else { + unset($data['password']); + } + + $user = $this->repository->update($id, $data); + + // Trigger user update event + event(new UserUpdatedEvent($user)); + + return $user->toArray(); + } + + /** + * Import users from file + */ + public function importFromFile($file): array + { + $filePath = $file->getPath() . '/' . $file->getFilename(); + + // Read Excel file + $data = $this->parseExcelFile($filePath); + + $successCount = 0; + $errorCount = 0; + $errors = []; + + foreach ($data as $index => $row) { + try { + $this->create([ + 'username' => $row['username'], + 'email' => $row['email'], + 'phone' => $row['phone'] ?? null, + 'password' => $row['password'] ?? '123456', + ]); + $successCount++; + } catch (\Exception $e) { + $errorCount++; + $errors[] = "Row {$index}: " . $e->getMessage(); + } + } + + return [ + 'success_count' => $successCount, + 'error_count' => $errorCount, + 'errors' => $errors + ]; + } + + /** + * Export users to file + */ + public function exportToFile(array $params = []): string + { + $users = $this->repository->getAllForExport($params); + + // Generate Excel file + $filePath = $this->generateExcelFile($users); + + return $filePath; + } + + /** + * Generate user avatar + */ + private function generateAvatar(string $username): string + { + // Use third-party library to generate avatar + $avatar = new \Intervention\Image\ImageManager(); + // ... Avatar generation logic + + return '/uploads/avatars/' . $username . '.png'; + } + + /** + * Parse Excel file + */ + private function parseExcelFile(string $filePath): array + { + // Excel parsing logic + return []; + } + + /** + * Generate Excel file + */ + private function generateExcelFile(array $users): string + { + // Excel generation logic + return '/tmp/users_export_' . date('YmdHis') . '.xlsx'; + } + + protected function getRepository(): string + { + return UserRepository::class; + } +} +``` + +#### 5. Repository (src/Repository/UserRepository.php) + +```php +getModel()::query(); + + // Keyword search + if (!empty($params['keyword'])) { + $query->where(function ($q) use ($params) { + $q->where('username', 'like', "%{$params['keyword']}%") + ->orWhere('email', 'like', "%{$params['keyword']}%") + ->orWhere('phone', 'like', "%{$params['keyword']}%"); + }); + } + + // Status filter + if (isset($params['status'])) { + $query->where('status', $params['status']); + } + + // Role filter + if (!empty($params['role_id'])) { + $query->whereHas('roles', function ($q) use ($params) { + $q->where('id', $params[' \ No newline at end of file diff --git a/docs/en/plugin/guide.md b/docs/en/plugin/guide.md new file mode 100644 index 0000000..15ccbe1 --- /dev/null +++ b/docs/en/plugin/guide.md @@ -0,0 +1,301 @@ +# Quick Start Guide + +This guide will help you quickly create your first MineAdmin plugin, covering the complete process from environment setup to plugin publishing. + +## Prerequisites + +Before you begin, ensure you have: + +1. **Installed MineAdmin**: Make sure the MineAdmin system is running properly +2. **Familiarity with the tech stack**: + - PHP 8.1+ and Hyperf framework + - Vue 3 + TypeScript (if frontend development is needed) + - Composer package manager + +## Environment Configuration + +### 1. Obtain AccessToken + +AccessToken is required for plugin marketplace and developer features: + +1. Log in to [MineAdmin Official Website](https://www.mineadmin.com/login) +2. Go to [Personal Center Settings](https://www.mineadmin.com/member/setting) +3. View and copy your AccessToken + +### 2. Configure Environment Variables + +Add the following to the `.env` file in the project root directory: + +```ini +# MineAdmin AccessToken +MINE_ACCESS_TOKEN=YourAccessToken +``` + +### 3. Initialize the Plugin System + +If using the plugin system for the first time, initialization is required: + +```bash +# Initialize the plugin extension system (MineAdmin 3.0+ versions are initialized by default) +php bin/hyperf.php mine-extension:initial +``` + +## Create Your First Plugin + +### 1. Create a Plugin Using Command Line + +```bash +# Create a mixed-type plugin +php bin/hyperf.php mine-extension:create mycompany/hello-world \ + --name "Hello World" \ + --type mixed \ + --author "Your Name" \ + --description "My first MineAdmin plugin" +``` + +**Parameter Explanation**: +- `mycompany/hello-world`: Plugin path (namespace/plugin name) +- `--name`: Plugin display name +- `--type`: Plugin type (mixed/backend/frontend) +- `--author`: Author name +- `--description`: Plugin description + +### 2. Generated Directory Structure + +After execution, the following structure will be generated in `plugin/mycompany/hello-world/`: + +``` +plugin/mycompany/hello-world/ +├── mine.json # Plugin configuration file +├── src/ # Backend source directory +│ ├── ConfigProvider.php # Configuration provider +│ ├── InstallScript.php # Installation script +│ └── UninstallScript.php # Uninstallation script +├── web/ # Frontend source directory +└── Database/ # Database related + ├── Migrations/ # Database migrations + └── Seeders/ # Data seeders +``` + +## Develop Your Plugin + +### 1. Configure Plugin Information + +Edit the `mine.json` file to complete plugin information: + +```json +{ + "name": "mycompany/hello-world", + "description": "My first MineAdmin plugin", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "Your Name", + "role": "developer" + } + ], + "composer": { + "psr-4": { + "Plugin\\MyCompany\\HelloWorld\\": "src" + }, + "config": "Plugin\\MyCompany\\HelloWorld\\ConfigProvider" + } +} +``` + +### 2. Implement the Configuration Provider + +Edit `src/ConfigProvider.php`: + +```php + [ + // Dependency injection configuration + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'publish' => [ + // Configuration file publishing settings + ], + ]; + } +} +``` + +### 3. Add Business Logic + +Create a controller `src/Controller/HelloController.php`: + +```php + 200, + 'message' => 'Hello from MineAdmin Plugin!', + 'data' => [ + 'plugin' => 'hello-world', + 'timestamp' => time() + ] + ]; + } +} +``` + +### 4. Frontend Development (Optional) + +Add frontend components in the `web/` directory: + +```vue + + + + +``` + +## Install and Test the Plugin + +### 1. Install the Plugin + +```bash +# Install the plugin into the system +php bin/hyperf.php mine-extension:install mycompany/hello-world --yes +``` + +### 2. Test Functionality + +Start the development server and test the API: + +```bash +# Start the service +php bin/hyperf.php start + +# Test the API (new terminal) +curl http://localhost:9501/hello-world/greeting +``` + +### 3. Check Installation Status + +```bash +# View locally installed plugins +php bin/hyperf.php mine-extension:local-list +``` + +## Plugin Management Commands + +### Common Command Overview + +```bash +# View remote plugin list +php bin/hyperf.php mine-extension:list + +# Download remote plugin +php bin/hyperf.php mine-extension:download --name plugin-name + +# Install local plugin +php bin/hyperf.php mine-extension:install plugin/path --yes + +# Uninstall plugin +php bin/hyperf.php mine-extension:uninstall plugin/path --yes + +# View local plugins +php bin/hyperf.php mine-extension:local-list +``` + +## Development Debugging Tips + +### 1. Log Debugging + +Use Hyperf's logging system in your plugin: + +```php +use Hyperf\Logger\LoggerFactory; + +$logger = $container->get(LoggerFactory::class)->get('plugin'); +$logger->info('Hello World Plugin Debug', ['data' => $someData]); +``` + +### 2. Configuration Hot Reload + +Restart the service after modifying configurations during development: + +```bash +# Restart Hyperf service +php bin/hyperf.php start +``` + +### 3. Frontend Hot Updates + +If using MineAdmin frontend development environment: + +```bash +# In the frontend project directory +npm run dev +``` + +## Next Steps + +Now you've created your first plugin! Next you can: + +1. [Learn more about plugin structure](./structure.md) +2. [Understand the complete development process](./develop.md) +3. [Learn about lifecycle management](./lifecycle.md) +4. [View more examples](./examples.md) + +## Frequently Asked Questions + +### Q: What if plugin installation fails? +A: Check if the `mine.json` configuration is correct and ensure PSR-4 autoload paths are properly set. + +### Q: How to debug plugins? +A: Use Hyperf's logging system and debugging tools, check log files in the `runtime/logs/` directory. + +### Q: Frontend components not displaying? +A: Ensure frontend files are placed in the `web/` directory, they will be automatically copied to the frontend project during plugin installation. \ No newline at end of file diff --git a/docs/en/plugin/index.md b/docs/en/plugin/index.md index 39bd3a8..8e79484 100644 --- a/docs/en/plugin/index.md +++ b/docs/en/plugin/index.md @@ -1,49 +1,117 @@ -# Preparation Work +# MineAdmin Plugin System -::: tip -To develop MineAdmin applications, first familiarize yourself with the MineAdmin and Hyperf frameworks, then complete the following preparatory steps. -::: +The MineAdmin plugin system provides powerful extensibility, allowing developers to create reusable functional modules to achieve system modularity and scalability. + +## Plugin System Architecture + +MineAdmin's plugin system is based on the Hyperf framework's ConfigProvider mechanism, offering complete plugin lifecycle management and automated deployment capabilities. + +```plantuml +@startuml +!define RECTANGLE class + +RECTANGLE "MineAdmin Core" as core { + + bin/hyperf.php + + Plugin::init() +} + +RECTANGLE "Plugin Management" as mgmt { + + App-Store Component + + Extension Commands + + Plugin Loader +} + +RECTANGLE "Plugin Structure" as structure { + + mine.json (Configuration file) + + src/ (Backend code) + + web/ (Frontend code) + + Database/ (Database) +} -## Obtain AccessToken +RECTANGLE "Official Plugins" as official { + + app-store +} -MineAdmin requires an `ACCESS_TOKEN` for downloading plugin applications, updating plugin applications, or developing plugin applications. +core --> mgmt : Plugin initialization +mgmt --> structure : Load plugins +mgmt --> official : Manage official plugins +structure --> core : Register services -Steps to obtain: +@enduml +``` -- Log in to the [MineAdmin](https://www.mineadmin.com/login) official website. -- Navigate to the [_Settings_](https://www.mineadmin.com/member/setting) page in the `Personal Center`. -- Click to view `My AccessToken`. +## Core Components -::: danger +### 1. Plugin Loader +- **File**: `bin/hyperf.php` ([GitHub](https://github.com/mineadmin/mineadmin/blob/master/bin/hyperf.php)) +- **Principle**: Automatically loads all installed plugins during application startup via `Plugin::init()` +- **Implementation**: Scans all plugins in the `plugin/` directory and registers their ConfigProvider ---- +### 2. App-Store Component +- **Repository**: [mineadmin/appstore](https://github.com/mineadmin/appstore) +- **Functionality**: Provides plugin download, installation, uninstallation, update, and other management features +- **Configuration**: Registers services and configurations via `ConfigProvider` -**Warning** +### 3. Plugin Configuration System +- **Core File**: `mine.json` +- **Principle**: Defines plugin metadata, dependencies, installation scripts, and other information +- **Loading**: Parses and registers into the system during plugin installation -Please safeguard your AccessToken and avoid leakage!!! +## Official Plugins ---- +MineAdmin provides the following official plugins by default: -::: +| Plugin Name | Description | Repository | +|------------|-------------|------------| +| app-store | Application marketplace management plugin, providing plugin download, installation, uninstallation, update, and other management features | [GitHub](https://github.com/mineadmin/appstore) | -## Configure the Backend .env File +> Note: Other plugins such as code generators, scheduled task managers, etc., can be obtained from the application marketplace or developed independently. -Open the _.env_ file in the backend root directory, locate the **MINE_ACCESS_TOKEN** entry, and paste the copied string after the **equals sign**. +## Plugin Types -```ini [.env] -APP_NAME = MineAdmin +MineAdmin supports three types of plugins: -APP_ENV = dev +### Mixed (Hybrid Plugins) +Plugins containing both frontend and backend complete functionalities, providing full business modules. -# Omitted... +### Backend (Backend Plugins) +Plugins containing only backend logic, primarily providing API services and business logic. -MINE_ACCESS_TOKEN = 107299501236086 -``` +### Frontend (Frontend Plugins) +Plugins containing only frontend interfaces, primarily providing user interface components. + +## Getting Started + +### Environment Setup + +Developing MineAdmin plugins requires: + +1. **Familiarity with the tech stack**: MineAdmin and Hyperf framework +2. **Obtaining AccessToken**: + - Log in to [MineAdmin Official Website](https://www.mineadmin.com/login) + - Go to Personal Center → [Settings Page](https://www.mineadmin.com/member/setting) + - Obtain AccessToken + +3. **Configuring Environment Variables**: +```ini +# .env file +MINE_ACCESS_TOKEN=YourAccessToken +``` + +::: Warning +Please keep your AccessToken secure to avoid leakage! +::: -## Apply for Developer Status +### Developer Authentication -If you are only developing applications locally for personal use, developer certification is not required, and you may distribute them to others freely. +- **Local Development**: No authentication required; free to develop and distribute +- **Marketplace Release**: Requires developer authentication; contact the MineAdmin team to obtain permissions -If you intend to publish your applications on the official marketplace, you must complete developer certification before submission, ensuring your work is protected under official copyright. +## Related Documentation -Currently, online certification applications are not supported. Please contact **MineAdmin team members** to request developer permissions. \ No newline at end of file +- [Quick Start Guide](./guide.md) - Create your first plugin +- [Development Guide](./develop.md) - Detailed development process +- [Plugin Structure](./structure.md) - Directory structure specifications +- [Lifecycle Management](./lifecycle.md) - Installation and uninstallation process +- [API Reference](./api.md) - Interface documentation +- [Example Code](./examples.md) - Practical cases \ No newline at end of file diff --git a/docs/en/plugin/lifecycle.md b/docs/en/plugin/lifecycle.md new file mode 100644 index 0000000..13f94df --- /dev/null +++ b/docs/en/plugin/lifecycle.md @@ -0,0 +1,700 @@ +# Plugin Lifecycle Management + +Detailed explanation of MineAdmin plugin lifecycle management, including the complete processes of installation, enabling, disabling, updating, and uninstallation. + +## Lifecycle Overview + +The lifecycle of MineAdmin plugins includes the following stages: + +```plantuml +@startuml +!define RECTANGLE class + +state "Uninstalled" as uninstalled +state "Downloaded" as downloaded +state "Installed" as installed +state "Enabled" as enabled +state "Disabled" as disabled +state "Update Required" as needUpdate +state "Uninstalled" as uninstalled2 + +[*] --> uninstalled +uninstalled --> downloaded : mine-extension:download +downloaded --> installed : mine-extension:install +installed --> enabled : Auto-enable +enabled --> disabled : Disable plugin +disabled --> enabled : Enable plugin +enabled --> needUpdate : New version detected +needUpdate --> enabled : mine-extension:update +enabled --> uninstalled2 : mine-extension:uninstall +disabled --> uninstalled2 : mine-extension:uninstall +uninstalled2 --> [*] + +note right of installed : Execute InstallScript +note right of uninstalled2 : Execute UninstallScript + +@enduml +``` + +## Plugin Discovery and Loading + +### 1. Plugin Discovery Mechanism + +**Core Implementation**: `Plugin::init()` method called in `bin/hyperf.php` ([GitHub](https://github.com/mineadmin/mineadmin/blob/master/bin/hyperf.php)) + +```plantuml +@startuml +participant "Application" as app +participant "Plugin::init()" as plugin +participant "ConfigProvider" as config +participant "Hyperf Container" as container + +app -> plugin : Called during application startup +plugin -> plugin : Scan plugin/ directory +plugin -> plugin : Read mine.json configuration +plugin -> config : Load ConfigProvider +config -> container : Register services to container +container -> app : Services available + +note right of plugin : Only load installed plugins\n(existence of install.lock file) + +@enduml +``` + +### 2. Loading Process Details + +1. **Scan Plugin Directory**: Traverse all subdirectories under `plugin/` +2. **Check Installation Status**: Verify existence of `install.lock` file +3. **Read Configuration**: Parse `mine.json` configuration file +4. **Load ConfigProvider**: Register plugin services to Hyperf container +5. **Register Routes**: Automatically register controller routes +6. **Load Middleware**: Register plugin middleware +7. **Register Event Listeners**: Load event listeners + +## Download Phase + +### Command Usage + +```bash +# Download specified plugin +php bin/hyperf.php mine-extension:download --name plugin-name + +# View downloadable plugin list +php bin/hyperf.php mine-extension:list +``` + +### Download Process + +1. **Verify AccessToken**: Check `MINE_ACCESS_TOKEN` environment variable +2. **Request Remote Repository**: Fetch plugin info from MineAdmin official repository +3. **Download Plugin Package**: Download zip package to local temp directory +4. **Extract Files**: Unzip to `plugin/vendor/plugin-name/` directory +5. **Verify Integrity**: Check if `mine.json` exists with correct format + +### Implementation Principle + +**Core Service**: App-Store component ([GitHub](https://github.com/mineadmin/appstore)) provides download functionality + +```php +// Pseudo-code example +class DownloadService +{ + public function download(string $pluginName): bool + { + // 1. Verify access token + $this->validateAccessToken(); + + // 2. Get plugin info + $pluginInfo = $this->getPluginInfo($pluginName); + + // 3. Download package + $packagePath = $this->downloadPackage($pluginInfo['download_url']); + + // 4. Extract to target directory + $this->extractPackage($packagePath, $this->getPluginPath($pluginName)); + + return true; + } +} +``` + +## Installation Phase + +### Command Usage + +```bash +# Install plugin +php bin/hyperf.php mine-extension:install vendor/plugin-name --yes + +# Force reinstall +php bin/hyperf.php mine-extension:install vendor/plugin-name --force +``` + +### Installation Process Details + +> ⚠️ **Important**: Configuration file publishing, environment checks, and database migrations should be handled in `InstallScript`, not relying on ConfigProvider's publish functionality. + +```plantuml +@startuml +start + +:Check plugin directory; +if (Directory exists?) then (Yes) + :Read mine.json config; + if (Config valid?) then (Yes) + :Check dependencies; + if (Dependencies met?) then (Yes) + :Install Composer dependencies; + :Copy frontend files; + #pink:Execute InstallScript; + note right + InstallScript handles: + - Environment checks + - Config file publishing + - Database migrations + - Initial data seeding + end note + if (InstallScript success?) then (Yes) + :Create install.lock; + :Register to plugin list; + :Trigger installation event; + :Clear cache; + stop + else (Failed) + :Rollback operations; + :Clean temp files; + stop + endif + else (Not met) + :Prompt to install dependencies; + stop + endif + else (Invalid) + :Report config error; + stop + endif +else (No) + :Report plugin missing; + stop +endif + +@enduml +``` + +### 1. Pre-installation Checks + +```php +// Pre-installation check logic +class InstallChecker +{ + public function check(string $pluginPath): array + { + $errors = []; + + // Check plugin directory + if (!is_dir($pluginPath)) { + $errors[] = 'Plugin directory not found'; + } + + // Check mine.json + $configPath = $pluginPath . '/mine.json'; + if (!file_exists($configPath)) { + $errors[] = 'mine.json config file not found'; + } + + // Check dependencies + $config = json_decode(file_get_contents($configPath), true); + foreach ($config['require'] ?? [] as $dependency => $version) { + if (!$this->isDependencyMet($dependency, $version)) { + $errors[] = "Dependency {$dependency} version {$version} not satisfied"; + } + } + + return $errors; + } +} +``` + +### 2. Composer Dependency Installation + +The installation process handles plugin Composer dependencies: + +```json +// Composer config in mine.json +{ + "composer": { + "require": { + "hyperf/async-queue": "^3.0", + "symfony/console": "^6.0" + }, + "psr-4": { + "Plugin\\Vendor\\PluginName\\": "src" + } + } +} +``` + +The system automatically executes: +```bash +composer require hyperf/async-queue:^3.0 symfony/console:^6.0 +``` + +### 3. InstallScript Handling ⭐ + +> **Best Practice**: Database migrations, config publishing, and environment checks should be handled in `InstallScript`: + +```php +// Handle all installation logic in InstallScript +class InstallScript +{ + public function handle(): bool + { + // 1. Environment check + if (!$this->checkEnvironment()) { + echo "Environment requirements not met\n"; + return false; + } + + // 2. Publish config files (not using ConfigProvider publish) + $this->publishConfig(); + + // 3. Run database migrations + if (!$this->runMigrations()) { + echo "Database migration failed\n"; + return false; + } + + // 4. Initialize data + $this->seedData(); + + return true; + } + + private function publishConfig(): void + { + $source = __DIR__ . '/../publish/config/plugin.php'; + $target = BASE_PATH . '/config/autoload/plugin.php'; + + if (!file_exists($target)) { + copy($source, $target); + echo "Config file published\n"; + } + } + + private function runMigrations(): bool + { + $migrationPath = __DIR__ . '/../Database/Migrations'; + + if (is_dir($migrationPath)) { + // Use Hyperf migration command + $container = \Hyperf\Context\ApplicationContext::getContainer(); + $application = $container->get(\Hyperf\Contract\ApplicationInterface::class); + + $input = new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'migrate', + '--path' => $migrationPath, + ]); + + $output = new \Symfony\Component\Console\Output\BufferedOutput(); + $exitCode = $application->run($input, $output); + + return $exitCode === 0; + } + + return true; + } +} +``` + +### 4. Frontend File Copying + +Copy files from `web/` directory to frontend project: + +``` +plugin/vendor/plugin-name/web/ → Frontend project directory +├── views/example.vue → src/views/plugin/vendor/plugin-name/example.vue +├── components/ExampleComp.vue → src/components/plugin/vendor/plugin-name/ExampleComp.vue +└── api/example.js → src/api/plugin/vendor/plugin-name/example.js +``` + +### 5. Config File Publishing ⚠️ + +> **Note**: The `publish` functionality in ConfigProvider is unreliable in plugin system, handle manually in InstallScript: + +```php +// Not recommended: publish in ConfigProvider may not work +'publish' => [ + // This approach may not execute in plugins +] + +// Recommended: Manual publishing in InstallScript +protected function publishConfig(): void +{ + $configs = [ + [ + 'source' => __DIR__ . '/../publish/config/plugin.php', + 'target' => BASE_PATH . '/config/autoload/plugin.php', + ], + [ + 'source' => __DIR__ . '/../publish/config/routes.php', + 'target' => BASE_PATH . '/config/routes/plugin.php', + ], + ]; + + foreach ($configs as $config) { + if (!file_exists($config['target'])) { + copy($config['source'], $config['target']); + echo "Config file published: {$config['target']}\n"; + } + } +} +``` + +### 6. Create Installation Lock File + +After successful installation, create `install.lock` to mark installation status: + +``` +plugin/vendor/plugin-name/install.lock +``` + +File contains installation info: +```json +{ + "installed_at": "2024-01-01 12:00:00", + "version": "1.0.0", + "installer": "admin", + "checksum": "abc123..." +} +``` + +## Enable/Disable Management + +### Plugin State Control + +MineAdmin supports temporarily disabling plugins without uninstalling: + +```bash +# Disable plugin +php bin/hyperf.php mine-extension:disable vendor/plugin-name + +# Enable plugin +php bin/hyperf.php mine-extension:enable vendor/plugin-name + +# Check plugin status +php bin/hyperf.php mine-extension:status vendor/plugin-name +``` + +### State Management Mechanism + +State information stored in `install.lock` file: + +```json +{ + "installed_at": "2024-01-01 12:00:00", + "version": "1.0.0", + "status": "enabled", // enabled | disabled + "disabled_at": null, + "disabled_reason": null +} +``` + +## Update Phase + +### Update Check + +```bash +# Check for plugin updates +php bin/hyperf.php mine-extension:check-updates + +# Update specific plugin +php bin/hyperf.php mine-extension:update vendor/plugin-name + +# Update all plugins +php bin/hyperf.php mine-extension:update-all +``` + +### Update Process + +```plantuml +@startuml +start + +:Check remote version; +if (New version?) then (Yes) + :Backup current plugin; + :Download new version; + :Verify integrity; + :Execute pre-update script; + :Replace plugin files; + :Run database migrations; + :Update config files; + :Execute post-update script; + if (Update success?) then (Yes) + :Update version info; + :Clean backups; + :Trigger update event; + stop + else (Failed) + :Restore backup; + :Report error; + stop + endif +else (No) + :No update needed; + stop +endif + +@enduml +``` + +### Version Compatibility Handling + +Version compatibility checked during update: + +```php +class UpdateManager +{ + public function checkCompatibility(string $currentVersion, string $newVersion): bool + { + // Check major version compatibility + $current = $this->parseVersion($currentVersion); + $new = $this->parseVersion($newVersion); + + // Breaking changes possible with major version change + if ($current['major'] !== $new['major']) { + return $this->checkBreakingChanges($currentVersion, $newVersion); + } + + return true; + } +} +``` + +## Uninstallation Phase + +### Command Usage + +```bash +# Uninstall plugin +php bin/hyperf.php mine-extension:uninstall vendor/plugin-name --yes + +# Force uninstall (ignore errors) +php bin/hyperf.php mine-extension:uninstall vendor/plugin-name --force +``` + +### Uninstallation Process + +```plantuml +@startuml +start + +:Check plugin status; +if (Plugin installed?) then (Yes) + :Check dependencies; + if (Other plugins depend on it?) then (Yes) + :Prompt dependency conflict; + if (Force uninstall?) then (Yes) + :Continue uninstall; + else (No) + :Cancel uninstall; + stop + endif + endif + + :Execute UninstallScript; + :Drop database tables; + :Clean config files; + :Remove frontend files; + :Clear cache; + :Remove Composer dependencies; + :Delete plugin directory; + :Clean registration info; + :Trigger uninstall event; + stop +else (No) + :Plugin not installed; + stop +endif + +@enduml +``` + +### Uninstall Script Execution + +```php +// UninstallScript example +class UninstallScript +{ + public function handle(): bool + { + try { + // 1. Clean database + $this->cleanDatabase(); + + // 2. Clean config files + $this->cleanConfigFiles(); + + // 3. Clean cache data + $this->cleanCache(); + + // 4. Clean log files + $this->cleanLogs(); + + // 5. Execute custom cleanup + $this->customCleanup(); + + return true; + } catch (\Exception $e) { + logger()->error('Plugin uninstall failed: ' . $e->getMessage()); + return false; + } + } + + private function cleanDatabase(): void + { + // Drop plugin-related tables + DB::statement('DROP TABLE IF EXISTS plugin_example'); + + // Clean config data + DB::table('system_config')->where('key', 'like', 'plugin.example.%')->delete(); + } +} +``` + +## Error Handling and Rollback + +### Installation Error Rollback + +Automatic rollback if errors occur during installation: + +```php +class InstallRollback +{ + public function rollback(string $pluginPath, array $operations): void + { + foreach (array_reverse($operations) as $operation) { + try { + switch ($operation['type']) { + case 'database': + $this->rollbackDatabase($operation['data']); + break; + case 'files': + $this->rollbackFiles($operation['data']); + break; + case 'config': + $this->rollbackConfig($operation['data']); + break; + } + } catch (\Exception $e) { + logger()->error('Rollback failed: ' . $e->getMessage()); + } + } + } +} +``` + +### Dependency Conflict Resolution + +Handling strategies for plugin dependency conflicts: + +```php +class DependencyResolver +{ + public function resolveConflicts(array $conflicts): array + { + $solutions = []; + + foreach ($conflicts as $conflict) { + $solution = match($conflict['type']) { + 'version_conflict' => $this->resolveVersionConflict($conflict), + 'circular_dependency' => $this->resolveCircularDependency($conflict), + 'missing_dependency' => $this->resolveMissingDependency($conflict), + default => null + }; + + if ($solution) { + $solutions[] = $solution; + } + } + + return $solutions; + } +} +``` + +## Event System + +Various plugin lifecycle stages trigger corresponding events: + +### Event List + +```php +// Plugin lifecycle events +class PluginEvents +{ + const BEFORE_INSTALL = 'plugin.before_install'; + const AFTER_INSTALL = 'plugin.after_install'; + const BEFORE_UNINSTALL = 'plugin.before_uninstall'; + const AFTER_UNINSTALL = 'plugin.after_uninstall'; + const BEFORE_UPDATE = 'plugin.before_update'; + const AFTER_UPDATE = 'plugin.after_update'; + const ENABLED = 'plugin.enabled'; + const DISABLED = 'plugin.disabled'; +} +``` + +### Event Listener Example + +```php +use Hyperf\Event\Annotation\Listener; +use Hyperf\Event\Contract\ListenerInterface; + +#[Listener] +class PluginInstallListener implements ListenerInterface +{ + public function listen(): array + { + return [ + PluginEvents::AFTER_INSTALL, + ]; + } + + public function process(object $event): void + { + // Post-installation logic + logger()->info('Plugin installed', [ + 'plugin' => $event->getPluginName(), + 'version' => $event->getVersion() + ]); + + // Clear cache + $this->clearCache($event->getPluginName()); + + // Send notification + $this->sendNotification($event); + } +} +``` + +## Status Queries + +### View Plugin Status + +```bash +# View all local plugin statuses +php bin/hyperf.php mine-extension:local-list + +# View available remote plugins +php bin/hyperf.php mine-extension:list + +# View specific plugin details +php bin/hyperf.php mine-extension:info vendor/plugin-name +``` + +### Status Information Structure + +```json +{ + "name": "vendor/plugin-name", + "version": " \ No newline at end of file diff --git a/docs/en/plugin/structure.md b/docs/en/plugin/structure.md index a198e14..01d19b6 100644 --- a/docs/en/plugin/structure.md +++ b/docs/en/plugin/structure.md @@ -1,20 +1,471 @@ -# Plugin Directory Structure - -A standard plugin directory structure description - ---- - -Taking the [Plugin Creation Section](./create.md) as an example: - -```shell -- plugin/test/demo # Plugin root directory --- plugin/test/demo/src # Plugin backend directory ---- plugin/test/demo/src/InstallScript.php # Class methods executed during plugin installation ---- plugin/test/demo/src/UninstallScript.php # Class methods executed during plugin uninstallation ---- plugin/test/demo/src/ConfigProvider.php # Plugin configuration file (consistent with Hyperf's official configuration) --- plugin/test/demo/Database # Plugin database migration and seeder directory ---- plugin/test/demo/Database/Migrations # Plugin database migration files ---- plugin/test/demo/Database/Seeder # Plugin database seeder files --- plugin/test/demo/web # Plugin frontend directory --- plugin/test/demo/mine.json # Plugin core information file -``` \ No newline at end of file +# Plugin Directory Structure + +A detailed explanation of MineAdmin plugin's standard directory structure, file specifications, and organizational approach. + +## Standard Directory Structure + +A complete MineAdmin plugin directory structure is as follows: + +``` +plugin/vendor/plugin-name/ # Plugin root directory +├── mine.json # Core plugin configuration file ⭐ +├── README.md # Plugin documentation +├── LICENSE # License file +├── composer.json # Composer dependency config (optional) +├── src/ # Backend source directory ⭐ +│ ├── ConfigProvider.php # Configuration provider ⭐ +│ ├── InstallScript.php # Installation script ⭐ +│ ├── UninstallScript.php # Uninstallation script ⭐ +│ ├── Controller/ # Controller directory +│ │ ├── AdminController.php # Admin controller +│ │ └── ApiController.php # API controller +│ ├── Service/ # Service layer directory +│ │ └── ExampleService.php # Business service class +│ ├── Repository/ # Repository layer directory +│ │ └── ExampleRepository.php # Data repository class +│ ├── Model/ # Model directory +│ │ └── Example.php # Data model +│ ├── Request/ # Request validation directory +│ │ ├── CreateRequest.php # Create request validation +│ │ └── UpdateRequest.php # Update request validation +│ ├── Resource/ # Resource transformation directory +│ │ └── ExampleResource.php # Resource transformation class +│ ├── Middleware/ # Middleware directory +│ │ └── ExampleMiddleware.php # Custom middleware +│ ├── Command/ # Command line directory +│ │ └── ExampleCommand.php # Custom command +│ ├── Listener/ # Event listener directory +│ │ └── ExampleListener.php # Event listener +│ └── Exception/ # Exception handling directory +│ └── ExampleException.php # Custom exception +├── web/ # Frontend source directory ⭐ +│ ├── views/ # Page components directory +│ │ ├── index.vue # Main page +│ │ ├── list.vue # List page +│ │ └── form.vue # Form page +│ ├── components/ # Common components directory +│ │ └── ExampleComponent.vue # Shared component +│ ├── api/ # API interface directory +│ │ └── example.js # Interface definition +│ ├── router/ # Routing configuration directory +│ │ └── index.js # Routing configuration +│ ├── store/ # State management directory +│ │ └── example.js # State management +│ └── assets/ # Static resources directory +│ ├── images/ # Image resources +│ └── styles/ # Style files +├── Database/ # Database-related directory ⭐ +│ ├── Migrations/ # Database migration files +│ │ └── 2024_01_01_000000_create_example_table.php +│ └── Seeders/ # Data seeder files +│ └── ExampleSeeder.php # Data seeder class +├── config/ # Configuration file directory +│ └── example.php # Plugin configuration file +├── publish/ # Publish files directory +│ ├── config/ # Configuration file templates +│ │ └── example.php # Configuration file template +│ └── assets/ # Static resource templates +├── tests/ # Test files directory +│ ├── Unit/ # Unit tests +│ ├── Feature/ # Feature tests +│ └── TestCase.php # Test base class +├── docs/ # Documentation directory +│ ├── installation.md # Installation docs +│ ├── usage.md # Usage docs +│ └── api.md # API docs +└── .gitignore # Git ignore file +``` + +## Core File Details + +### 1. mine.json (Plugin Configuration File) + +**File Path**: `mine.json` ([Configuration Details](./mineJson.md)) + +Core configuration file defining plugin metadata, dependencies and loading configuration: + +```json +{ + "name": "vendor/plugin-name", + "description": "Plugin description", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "Author Name", + "email": "author@example.com", + "role": "developer" + } + ], + "keywords": ["mineadmin", "plugin"], + "homepage": "https://github.com/vendor/plugin-name", + "license": "MIT", + "require": { + "php": ">=8.1", + "hyperf/framework": "^3.0" + }, + "package": { + "dependencies": { + "vue": "^3.0", + "element-plus": "^2.0" + } + }, + "composer": { + "require": { + "hyperf/async-queue": "^3.0" + }, + "psr-4": { + "Plugin\\Vendor\\PluginName\\": "src" + }, + "config": "Plugin\\Vendor\\PluginName\\ConfigProvider" + } +} +``` + +### 2. ConfigProvider.php (Configuration Provider) + +**File Path**: `src/ConfigProvider.php` +**Implementation**: Based on Hyperf ConfigProvider mechanism ([GitHub](https://github.com/hyperf/hyperf/blob/master/src/config-provider/src/ConfigProvider.php)) + +> ⚠️ **Note**: The `publish` feature in ConfigProvider has issues in plugin system, recommend handling config publishing in InstallScript. + +```php + [], + 'annotations' => [ + 'scan' => [ + 'paths' => [__DIR__], + ], + ], + 'commands' => [], + 'listeners' => [], + // publish feature not recommended in plugins + // Handle config publishing in InstallScript instead + ]; + } +} +``` + +### 3. InstallScript.php (Installation Script) ⭐ + +**File Path**: `src/InstallScript.php` +**Execution Timing**: When running `mine-extension:install` command +**Importance**: Recommended for handling config publishing, environment checks and database migrations + +```php +checkEnvironment()) { + echo "Environment check failed\n"; + return false; + } + + // 2. Publish config files + $this->publishConfig(); + + // 3. Run database migrations + $this->runMigrations(); + + // 4. Initialize data + $this->seedData(); + + echo "Plugin installed successfully\n"; + return true; + } + + protected function checkEnvironment(): bool + { + // Check PHP version + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + echo "PHP version needs >= 8.1\n"; + return false; + } + + // Check required extensions + $requiredExtensions = ['redis', 'pdo', 'json']; + foreach ($requiredExtensions as $ext) { + if (!extension_loaded($ext)) { + echo "Missing PHP extension: {$ext}\n"; + return false; + } + } + + return true; + } + + protected function publishConfig(): void + { + $source = __DIR__ . '/../publish/config/plugin.php'; + $target = BASE_PATH . '/config/autoload/plugin.php'; + + if (!file_exists($target)) { + copy($source, $target); + echo "Config file published: {$target}\n"; + } + } + + protected function runMigrations(): void + { + $migrationPath = __DIR__ . '/../Database/Migrations'; + + if (is_dir($migrationPath)) { + // Execute migration command + $container = \Hyperf\Context\ApplicationContext::getContainer(); + $application = $container->get(ApplicationInterface::class); + $application->setAutoExit(false); + + $input = new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'migrate', + '--path' => $migrationPath, + ]); + + $output = new \Symfony\Component\Console\Output\BufferedOutput(); + $application->run($input, $output); + + echo "Database migrations completed\n"; + } + } + + protected function seedData(): void + { + // Initialize default data + // e.g. create default configs, menus etc. + } +} +``` + +### 4. UninstallScript.php (Uninstallation Script) ⭐ + +**File Path**: `src/UninstallScript.php` +**Execution Timing**: When running `mine-extension:uninstall` command +**Importance**: Cleans up config files, database tables and related resources + +```php +backupData(); + + // 2. Drop database tables + $this->dropTables(); + + // 3. Clean up config files + $this->removeConfig(); + + // 4. Clear cache + $this->clearCache(); + + echo "Plugin uninstalled\n"; + return true; + } + + protected function backupData(): void + { + // Backup important data to specified directory + $backupPath = BASE_PATH . '/runtime/backup/plugin_' . date('YmdHis') . '.sql'; + // Implement backup logic + } + + protected function dropTables(): void + { + // Drop plugin-created database tables + $tables = ['plugin_example_table', 'plugin_settings']; + + foreach ($tables as $table) { + if (Db::schema()->hasTable($table)) { + Db::schema()->drop($table); + echo "Dropped table: {$table}\n"; + } + } + } + + protected function removeConfig(): void + { + $configFile = BASE_PATH . '/config/autoload/plugin.php'; + + if (file_exists($configFile)) { + unlink($configFile); + echo "Config file removed: {$configFile}\n"; + } + } + + protected function clearCache(): void + { + // Clear plugin-related cache + $redis = \Hyperf\Context\ApplicationContext::getContainer() + ->get(\Hyperf\Redis\Redis::class); + + $redis->del('plugin:cache:*'); + echo "Cache cleared\n"; + } +} +``` + +## Directory Structure Diagram + +```plantuml +@startuml +!define FOLDER rectangle +!define FILE rectangle + +FOLDER "Plugin Root" as root { + FILE "mine.json" as config + FOLDER "src/" as src { + FILE "ConfigProvider.php" as provider + FILE "InstallScript.php" as install + FILE "UninstallScript.php" as uninstall + FOLDER "Controller/" as controller + FOLDER "Service/" as service + FOLDER "Model/" as model + } + FOLDER "web/" as web { + FOLDER "views/" as views + FOLDER "components/" as components + FOLDER "api/" as api + } + FOLDER "Database/" as database { + FOLDER "Migrations/" as migrations + FOLDER "Seeders/" as seeders + } +} + +config --> provider : Config loading +provider --> install : Called during install +provider --> uninstall : Called during uninstall +web --> views : Frontend pages +database --> migrations : Database schema +database --> seeders : Initial data + +@enduml +``` + +## Structure Differences by Plugin Type + +### Mixed (Hybrid Plugin) +Contains complete `src/` and `web/` directories, providing full frontend+backend functionality. + +### Backend (Backend Plugin) +Only contains `src/` directory, focused on API services and business logic: + +``` +plugin/vendor/backend-plugin/ +├── mine.json +├── src/ +│ ├── ConfigProvider.php +│ ├── Controller/ +│ ├── Service/ +│ └── Model/ +└── Database/ +``` + +### Frontend (Frontend Plugin) +Only contains `web/` directory, focused on UI and interactions: + +``` +plugin/vendor/frontend-plugin/ +├── mine.json +├── web/ +│ ├── views/ +│ ├── components/ +│ └── assets/ +└── src/ + └── ConfigProvider.php # Minimal config +``` + +## Naming Conventions + +### 1. Directory Naming +- Use lowercase with hyphens: `user-management` +- Avoid underscores and spaces + +### 2. File Naming +- PHP class files use PascalCase: `UserController.php` +- Vue components use PascalCase: `UserList.vue` +- Config files use lowercase: `user.php` + +### 3. Namespace Standards +Follow PSR-4 autoloading: + +```php +// Plugin path: plugin/mineadmin/user-manager/ +// Namespace: Plugin\MineAdmin\UserManager\ +namespace Plugin\MineAdmin\UserManager\Controller; +``` + +## File Permissions and Security + +### 1. File Permissions +```bash +# Set appropriate permissions +find plugin/ -type f -name "*.php" -exec chmod 644 {} \; +find plugin/ -type d -exec chmod 755 {} \; +``` + +### 2. Security Considerations +- Use environment variables for sensitive configs +- Avoid hardcoding secrets +- Validate and filter user input +- Use HTTPS for sensitive data + +## Best Practices + +### 1. File Organization +- Organize by feature modules +- Maintain clear structure +- Use meaningful filenames + +### 2. Code Standards +- Follow PSR-12 coding standard +- Add proper comments +- Use type declarations + +### 3. Version Control +- Use `.gitignore` for unnecessary files +- Create clear commit messages +- Use semantic versioning + +## Example Project Structure + +View official plugin structures: + +**App-Store Plugin**: MineAdmin official app store plugin demonstrating standard hybrid structure + +## FAQ + +### Q: Where should plugin directories be placed? +A: Plugins should be placed in project root's `plugin/` directory, organized as `vendor/plugin-name`. + +### Q: How to handle plugin dependencies? +A: Declare dependencies in `mine.json`'s `require` field. + +### Q: Where do frontend files go after installation? +A: Files under `web/` are copied to frontend project's corresponding locations. + +### Q: How are database migrations executed? +A: Call migration logic in `InstallScript.php` or use Hyperf's migration commands. \ No newline at end of file diff --git a/docs/ja/plugin/api.md b/docs/ja/plugin/api.md new file mode 100644 index 0000000..16ab9e9 --- /dev/null +++ b/docs/ja/plugin/api.md @@ -0,0 +1,632 @@ +# API リファレンスドキュメント + +このドキュメントでは、MineAdmin プラグインシステムのすべてのAPIインターフェース、コマンドラインツール、およびコアライブラリについて詳しく説明します。 + +## コマンドライン API + +### プラグイン管理コマンド + +#### 1. mine-extension:initial + +プラグイン拡張システムを初期化します。 + +```bash +php bin/hyperf.php mine-extension:initial +``` + +**機能**: +- app-store 設定ファイルの公開 +- プラグインシステム設定の初期化 +- 必要なディレクトリ構造の作成 + +**実装クラス**: `Mine\AppStore\Command\InitialCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/InitialCommand.php)) + +#### 2. mine-extension:list + +リモートプラグインのリストを検索します。 + +```bash +php bin/hyperf.php mine-extension:list [options] +``` + +**パラメータ**: +| パラメータ | タイプ | デフォルト値 | 説明 | +|------|------|--------|------| +| --type | string | all | 拡張タイプのフィルタリング (mixed/backend/frontend) | +| --name | string | - | 拡張名でフィルタリング | +| --category | string | - | カテゴリでフィルタリング | +| --author | string | - | 作者でフィルタリング | + +**例**: +```bash +# すべてのプラグインを表示 +php bin/hyperf.php mine-extension:list + +# 混合型プラグインを表示 +php bin/hyperf.php mine-extension:list --type=mixed + +# 特定のプラグインを検索 +php bin/hyperf.php mine-extension:list --name=user-manager +``` + +**実装クラス**: `Mine\AppStore\Command\ListCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/ListCommand.php)) + +#### 3. mine-extension:local-list + +ローカルのすべてのプラグインを検索します。 + +```bash +php bin/hyperf.php mine-extension:local-list [options] +``` + +**パラメータ**: +| パラメータ | タイプ | デフォルト値 | 説明 | +|------|------|--------|------| +| --status | string | all | ステータスでフィルタリング (installed/enabled/disabled) | +| --type | string | all | タイプでフィルタリング | + +**実装クラス**: `Mine\AppStore\Command\LocalListCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/LocalListCommand.php)) + +#### 4. mine-extension:download + +リモートプラグインをローカルにダウンロードします。 + +```bash +php bin/hyperf.php mine-extension:download --name=plugin-name [options] +``` + +**パラメータ**: +| パラメータ | タイプ | 必須 | 説明 | +|------|------|------|------| +| --name | string | はい | プラグイン名 | +| --version | string | いいえ | バージョンを指定 | +| --force | bool | いいえ | 既存のプラグインを強制的に上書き | + +**実装クラス**: `Mine\AppStore\Command\DownloadCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/DownloadCommand.php)) + +#### 5. mine-extension:install + +指定したプラグインをインストールします。 + +```bash +php bin/hyperf.php mine-extension:install {path} [options] +``` + +**パラメータ**: +| パラメータ | タイプ | 必須 | 説明 | +|------|------|------|------| +| path | string | はい | プラグインパス (vendor/plugin-name) | +| --yes | bool | いいえ | 確認プロンプトをスキップ | +| --force | bool | いいえ | 強制的に再インストール | +| --skip-dependencies | bool | いいえ | 依存関係チェックをスキップ | + +**例**: +```bash +# プラグインをインストール +php bin/hyperf.php mine-extension:install mineadmin/user-manager --yes + +# 強制的に再インストール +php bin/hyperf.php mine-extension:install mineadmin/user-manager --force +``` + +**実装クラス**: `Mine\AppStore\Command\InstallCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/InstallCommand.php)) + +#### 6. mine-extension:uninstall + +指定したプラグインをアンインストールします。 + +```bash +php bin/hyperf.php mine-extension:uninstall {path} [options] +``` + +**パラメータ**: +| パラメータ | タイプ | 必須 | 説明 | +|------|------|------|------| +| path | string | はい | プラグインパス | +| --yes | bool | いいえ | 確認プロンプトをスキップ | +| --force | bool | いいえ | 強制的にアンインストール (エラーを無視) | +| --keep-data | bool | いいえ | ユーザーデータを保持 | + +**実装クラス**: `Mine\AppStore\Command\UninstallCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/UninstallCommand.php)) + +#### 7. mine-extension:create + +新しいプラグインを作成します。 + +```bash +php bin/hyperf.php mine-extension:create {path} [options] +``` + +**パラメータ**: +| パラメータ | タイプ | デフォルト値 | 説明 | +|------|------|--------|------| +| path | string | - | プラグインパス (vendor/plugin-name) | +| --name | string | example | プラグイン表示名 | +| --type | string | mixed | プラグインタイプ (mixed/backend/frontend) | +| --author | string | - | 作者名 | +| --description | string | - | プラグインの説明 | +| --license | string | MIT | ライセンスタイプ | + +**例**: +```bash +php bin/hyperf.php mine-extension:create mycompany/hello-world \ + --name "Hello World" \ + --type mixed \ + --author "Your Name" \ + --description "私の最初のプラグイン" +``` + +**実装クラス**: `Mine\AppStore\Command\CreateCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/CreateCommand.php)) + +## コアライブラリ API + +### Plugin クラス + +**ファイル位置**: `Mine\AppStore\Plugin` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Plugin.php)) + +プラグインシステムのコアクラスで、プラグインの読み込みと管理を担当します。 + +#### Plugin::init() + +プラグインシステムを初期化し、アプリケーション起動時に呼び出されます。 + +```php + [ + 'name' => 'vendor/plugin-name', + 'version' => '1.0.0', + 'path' => '/path/to/plugin', + 'config' => [...], // mine.json 設定 + 'status' => 'enabled' + ] +] +``` + +#### Plugin::isInstalled() + +プラグインがインストールされているかどうかをチェックします。 + +```php +install('vendor/plugin-name', [ + 'force' => false, + 'skip_dependencies' => false +]); + +if ($result['success']) { + echo "インストール成功"; +} else { + echo "インストール失敗: " . $result['message']; +} +``` + +#### uninstall() + +プラグインをアンインストールします。 + +```php +uninstall('vendor/plugin-name', [ + 'force' => false, + 'keep_data' => false +]); +``` + +#### update() + +プラグインを更新します。 + +```php +update('vendor/plugin-name'); +``` + +### ConfigProvider 基底クラス + +すべてのプラグインの ConfigProvider は次のインターフェースに従う必要があります: + +```php + [ + InterfaceA::class => ImplementationA::class, + ], + + // アノテーションスキャンパス + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + + // コマンドラインコマンド + 'commands' => [ + CustomCommand::class, + ], + + // イベントリスナー + 'listeners' => [ + CustomListener::class, + ], + + // ミドルウェア + 'middlewares' => [ + 'http' => [ + CustomMiddleware::class, + ], + ], + + // 設定ファイル公開 + 'publish' => [ + [ + 'id' => 'config-id', + 'description' => '設定ファイルの説明', + 'source' => __DIR__ . '/../publish/config.php', + 'destination' => BASE_PATH . '/config/autoload/plugin.php', + ], + ], + + // プロセス設定 + 'processes' => [ + CustomProcess::class, + ], + ]; + } +} +``` + +## HTTP API + +### プラグイン管理インターフェース + +#### プラグインリストを取得 + +```http +GET /admin/plugin/list +``` + +**リクエストパラメータ**: +```json +{ + "page": 1, + "pageSize": 15, + "type": "mixed", + "status": "enabled", + "keyword": "search term" +} +``` + +**レスポンス例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "list": [ + { + "name": "vendor/plugin-name", + "display_name": "プラグイン表示名", + "version": "1.0.0", + "description": "プラグインの説明", + "author": "作者名", + "type": "mixed", + "status": "enabled", + "installed_at": "2024-01-01 12:00:00", + "updated_at": "2024-01-15 10:30:00" + } + ], + "total": 1 + } +} +``` + +#### プラグインをインストール + +```http +POST /admin/plugin/install +``` + +**リクエストパラメータ**: +```json +{ + "name": "vendor/plugin-name", + "version": "1.0.0", + "force": false +} +``` + +**レスポンス例**: +```json +{ + "code": 200, + "message": "インストール成功", + "data": { + "plugin": "vendor/plugin-name", + "version": "1.0.0", + "installed_at": "2024-01-01 12:00:00" + } +} +``` + +#### プラグインをアンインストール + +```http +DELETE /admin/plugin/uninstall +``` + +**リクエストパラメータ**: +```json +{ + "name": "vendor/plugin-name", + "keep_data": false +} +``` + +#### プラグインの有効化/無効化 + +```http +PUT /admin/plugin/toggle-status +``` + +**リクエストパラメータ**: +```json +{ + "name": "vendor/plugin-name", + "status": "enabled" // enabled | disabled +} +``` + +## イベント API + +### プラグインイベントシステム + +プラグインシステムは豊富なイベントフックを提供し、開発者がプラグインライフサイクルの重要なポイントでカスタムロジックを実行できるようにします。 + +#### イベントタイプ + +```php +success) { + // プラグインインストール成功後の処理 + $this->clearCache(); + $this->sendNotification($event->pluginName); + $this->updateStatistics($event->pluginName); + } else { + // インストール失敗時の処理 + logger()->error('プラグインインストール失敗', [ + 'plugin' => $event->pluginName, + 'error' => $event->error + ]); + } + } +} +``` + +## フック API + +### プラグインフックシステム + +MineAdmin はフックシステムを提供し、プラグインがシステムの重要なポイントにカスタムロジックを注入できるようにします。 + +#### フックを登録 + +```php +info('ユーザーログイン試行', ['user_id' => $user->id]); + }); + + HookManager::register('user.login.after', function($user) { + // ユーザーログイン後の処理ロジック + $this->recordLoginHistory($user); + }); + + return [ + // ... その他の設定 + ]; + } +} +``` + +#### フックをトリガー + +```php +authenticate($credentials); + + if ($result) { + // ログイン後フック + HookManager::trigger('user.login.after', $user); + } + + return $result; + } +} +``` + +#### 利用可能なフックリスト + +| フック名 | トリガー時機 | パラメータ | +|----------|----------|------| +| `user.login.before` | ユーザーログイン前 | User $user | +| `user.login.after` | ユーザ \ No newline at end of file diff --git a/docs/ja/plugin/develop.md b/docs/ja/plugin/develop.md new file mode 100644 index 0000000..0478e0f --- /dev/null +++ b/docs/ja/plugin/develop.md @@ -0,0 +1,552 @@ +# プラグイン開発ガイド + +このガイドは実際のMineAdmin公式プラグインコードに基づき、プラグインの完全な開発プロセスを詳細に説明します。 + +## 開発プロセスの概要 + +```plantuml +@startuml +!define PROCESS rectangle + +PROCESS "1. プラグイン構造の作成" as create +PROCESS "2. mine.jsonの設定" as config +PROCESS "3. ConfigProviderの記述" as provider +PROCESS "4. バックエンド開発" as backend +PROCESS "5. フロントエンド開発" as frontend +PROCESS "6. インストール/アンインストールスクリプト" as script +PROCESS "7. テストとデバッグ" as test +PROCESS "8. パッケージ化と公開" as publish + +create --> config +config --> provider +provider --> backend +provider --> frontend +backend --> script +frontend --> script +script --> test +test --> publish + +note right of create : mine-extension:create +note right of config : プラグインメタデータ設定 +note right of provider : サービスとルートの登録 +note right of backend : Controller + Service +note right of frontend : Vue3 + TypeScript +note right of script : InstallScript/UninstallScript +note right of test : ローカルインストールテスト +note right of publish : アプリケーションマーケットへの公開 + +@enduml +``` + +## プラグイン構造の規約 + +`app-store`と`code-generator`プラグインの実際のコードに基づき、MineAdminプラグインには2つの典型的な構造があります: + +### シンプルなプラグイン構造(純粋なバックエンドまたは単純な機能向け) + +``` +plugin/mine-admin/plugin-name/ +├── mine.json # プラグイン設定ファイル +├── install.lock # インストールマーカー(自動生成) +└── src/ + ├── ConfigProvider.php # 設定プロバイダー + ├── Controller/ # コントローラー + │ └── IndexController.php + └── Service/ # サービス層 + └── Service.php +``` + +### 完全なプラグイン構造(複雑なビジネス向け) + +``` +plugin/mine-admin/plugin-name/ +├── mine.json # プラグイン設定ファイル +├── install.lock # インストールマーカー(自動生成) +├── README.md # プラグイン説明 +├── src/ # バックエンドコード +│ ├── ConfigProvider.php # 設定プロバイダー +│ ├── InstallScript.php # インストールスクリプト +│ ├── UninstallScript.php # アンインストールスクリプト +│ ├── Http/ +│ │ ├── Controller/ # コントローラー +│ │ ├── Request/ # リクエスト検証 +│ │ └── Vo/ # 値オブジェクト +│ ├── Model/ # データモデル +│ ├── Repository/ # リポジトリ層 +│ └── Service/ # サービス層 +├── web/ # フロントエンドコード +│ ├── index.ts # プラグインエントリ +│ ├── api/ # APIインターフェース +│ ├── views/ # Vueコンポーネント +│ └── locales/ # 言語パック +├── Database/ # データベース +│ ├── Migrations/ # マイグレーションファイル +│ └── Seeder/ # シードデータ +├── languages/ # バックエンド言語パック +│ └── zh_CN/ +└── publish/ # 公開リソース + └── template/ # テンプレートファイル +``` + +## バックエンド開発 + +### 1. ConfigProvider 設定プロバイダー + +app-storeプラグインの実際の実装に基づく: + +```php + [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + // 依存注入(オプション) + 'dependencies' => [ + // Interface::class => Implementation::class + ], + // コマンドライン(オプション) + 'commands' => [ + // Command::class + ], + // ミドルウェア(オプション) + 'middlewares' => [ + 'http' => [ + // Middleware::class + ], + ], + // イベントリスナー(オプション) + 'listeners' => [ + // Listener::class + ], + ]; + } +} +``` + +### 2. コントローラー開発 + +app-storeのIndexController実装を参考: + +```php +success( + $this->service->getAppList($this->request->all()) + ); + } + + /** + * プラグインのダウンロード + */ + #[PostMapping("download")] + #[Permission("plugin:store:download")] + public function download(): ResponseInterface + { + $params = $this->request->all(); + $this->service->download($params); + return $this->success(); + } + + /** + * プラグインのインストール + */ + #[PostMapping("install")] + #[Permission("plugin:store:install")] + public function install(): ResponseInterface + { + $params = $this->request->all(); + $this->service->install($params); + return $this->success(); + } + + /** + * プラグインのアンインストール + */ + #[PostMapping("unInstall")] + #[Permission("plugin:store:uninstall")] + public function unInstall(): ResponseInterface + { + $params = $this->request->all(); + $this->service->unInstall($params); + return $this->success(); + } + + /** + * ローカルプラグインインストールリスト + */ + #[GetMapping("getInstallList")] + #[RemoteState] + public function getInstallList(): ResponseInterface + { + return $this->success( + $this->service->getLocalAppInstallList() + ); + } + + /** + * ローカルアップロードインストール + */ + #[PostMapping("uploadInstall")] + #[Permission("plugin:store:uploadInstall")] + public function uploadInstall(): ResponseInterface + { + return $this->success( + $this->service->uploadLocalApp($this->request->all()) + ); + } +} +``` + +**主要なアノテーション説明**: +- `#[Controller]`: コントローラールートプレフィックスの定義 +- `#[Auth]`: ログイン認証が必要 +- `#[Permission]`: 権限検証 +- `#[GetMapping]`/`#[PostMapping]`: ルートメソッドの定義 +- `#[Inject]`: 依存注入 +- `#[RemoteState]`: リモート状態管理 + +### 3. サービス層開発 + +app-storeのService実装パターンに基づく: + +```php +service->getAppList($params); + } + + /** + * アプリケーションのダウンロード + */ + public function download(array $params): void + { + $app = $this->service->getAppInfo($params['identifier']); + + if (empty($app['download_url'])) { + throw new MineException('このアプリケーションはダウンロードできません', 500); + } + + if (Plugin::hasLocalInstalled($params['identifier'])) { + throw new MineException('アプリケーションは既にローカルに存在します。再ダウンロードする場合は、まずローカルアプリケーションを削除してください', 500); + } + + $this->service->download($params); + } + + /** + * アプリケーションのインストール + */ + public function install(array $params): void + { + $pluginName = $params['name']; + + if (!Plugin::hasLocal($pluginName)) { + throw new MineException('プラグインが存在しません', 500); + } + + if (Plugin::hasLocalInstalled($pluginName)) { + throw new MineException('プラグインは既にインストールされています', 500); + } + + Plugin::forceRefreshJsonPath($pluginName); + Plugin::install($pluginName); + } + + /** + * アプリケーションのアンインストール + */ + public function unInstall(array $params): void + { + $pluginName = $params['name']; + + if (!Plugin::hasLocalInstalled($pluginName)) { + throw new MineException('プラグインがインストールされていません', 500); + } + + Plugin::uninstall($pluginName); + } + + /** + * ローカルにインストールされたプラグインリストの取得 + */ + public function getLocalAppInstallList(): array + { + $list = []; + $plugins = Plugin::getLocalPlugins(); + + foreach ($plugins as $name => $info) { + $app = ['identifier' => $name]; + $app['name'] = $info['name'] ?? '未知'; + $app['status'] = $info['status'] ?? false; + $app['version'] = $info['version'] ?? '0.0.0'; + $app['description'] = $info['description'] ?? '説明なし'; + $app['created_at'] = $info['created_at'] ?? ''; + $list[] = $app; + } + + return $list; + } + + /** + * ローカルアップロードインストール + */ + public function uploadLocalApp(array $params): void + { + if (empty($params['path'])) { + throw new MineException('プラグインパッケージをアップロードしてください', 500); + } + + // プラグインパッケージの解凍と検証 + $zipFile = new \ZipArchive(); + $result = $zipFile->open($params['path']); + + if ($result !== true) { + throw new MineException('プラグインパッケージの解凍に失敗しました', 500); + } + + // プラグイン情報の取得とインストール + $mineJson = $zipFile->getFromName('mine.json'); + if (!$mineJson) { + throw new MineException('プラグインパッケージ形式が不正です。mine.jsonが不足しています', 500); + } + + $config = json_decode($mineJson, true); + $pluginName = $config['name'] ?? null; + + if (!$pluginName) { + throw new MineException('プラグインパッケージの設定が不正です', 500); + } + + // プラグインディレクトリに解凍 + $targetPath = Plugin::getPluginPath($pluginName); + $zipFile->extractTo($targetPath); + $zipFile->close(); + + // キャッシュの更新とインストール + Plugin::forceRefreshJsonPath($pluginName); + Plugin::install($pluginName); + } +} +``` + +### 4. モデル層(データベースが必要な場合) + +code-generatorプラグインのモデル実装を参考: + +```php + 'boolean', + 'is_list' => 'boolean', + 'is_query' => 'boolean', + 'is_required' => 'boolean', + 'is_sort' => 'boolean', + 'is_edit' => 'boolean', + 'is_readonly' => 'boolean', + ]; +} +``` + +## フロントエンド開発 + +### 1. プラグインエントリファイル (index.ts) + +app-storeのフロントエンド実装に基づく: + +```typescript +import type { App } from 'vue' +import type { Plugin } from '#/global' + +const pluginConfig: Plugin.PluginConfig = { + install(app: App) { + // Vueプラグインインストールフック + console.log('app-store plugin install') + }, + config: { + enable: true, + info: { + name: 'app-store', + version: '1.0.0', + author: 'MineAdmin Team', + description: 'MineAdminアプリケーションマーケット可視化プラグイン' + } + }, + views: [ + { + name: 'plugin:store', + path: '/plugin/store', + meta: { + title: 'app_store.app_store', + i18n: true, + icon: 'material-symbols:app-shortcut', + type: 'M', + hidden: false, + componentPath: '/plugin/mine-admin/app-store/views/index.vue', + componentName: 'plugin:mine-admin:app-store:index', + }, + component: () => import('./views/index.vue'), + } + ], +} + +export default pluginConfig +``` + +### 2. API インターフェースのカプセル化 + +```typescript +// api/app-store.ts +import { request } from '@/utils/request' + +// リモートプラグインリストの取得 +export const getAppList = (params: any) => { + return request.get('/admin/plugin/store/index', { params }) +} + +// プラグインのダウンロード +export const downloadApp = (data: any) => { + return request.post('/admin/plugin/store/download', data) +} + +// プラグインのインストール +export const installApp = (data: any) => { + return request.post('/admin/plugin/store/install', data) +} + +// プラグインのアンインストール +export const uninstallApp = (data: any) => { + return request.post('/admin/plugin/store/unInstall', data) +} + +// ローカルにインストールされたプラグインの取得 +export const getInstalledList = () => { + return request.get('/admin/plugin/store/getInstallList') +} + +// ローカルアップロードインストール +export const uploadInstall = (data: any) => { + return request.post('/admin/plugin/store/uploadInstall', data) +} +``` + +### 3. Vue コンポーネント開発 + +```vue + + + + +``` + +### 4. 国際化サポート + +```typescript +// locales/ja_JP.ts +export default { + app_store: { + app_store: 'アプリケーションマーケット', + app_list: 'アプリケーションリスト', + installed: 'インストール済み', + install: 'インストール', + uninstall: 'アンインストール', + download: 'ダウンロード', + upload: 'アップロード', + local_upload: 'ローカルアッ \ No newline at end of file diff --git a/docs/ja/plugin/examples.md b/docs/ja/plugin/examples.md new file mode 100644 index 0000000..c6f406a --- /dev/null +++ b/docs/ja/plugin/examples.md @@ -0,0 +1,599 @@ +# プラグインサンプルコード + +このドキュメントでは、MineAdminプラグイン開発の完全なサンプルを提供します。さまざまなタイプのプラグインの実際のコード例とベストプラクティスが含まれています。 + +## 公式プラグインサンプル + +### App-Store プラグイン (ハイブリッド型) + +**リポジトリURL**: [mineadmin/appstore](https://github.com/mineadmin/appstore) + +App-StoreはMineAdminで唯一の公式デフォルトプラグインで、アプリケーションマーケット管理機能を提供し、ハイブリッド型プラグインの完全な実装を示しています。 + +#### コアファイル構造 +``` +plugin/mine-admin/app-store/ +├── mine.json # プラグイン設定 +├── src/ # バックエンドコード +│ ├── ConfigProvider.php # 設定プロバイダ +│ ├── Controller/ # コントローラ +│ ├── Service/ # サービス層 +│ └── Command/ # コマンドライン +├── web/ # フロントエンドコード +│ ├── views/ # ページコンポーネント +│ └── api/ # APIインターフェース +└── Database/ # データベース +``` + +#### mine.json 設定例 +```json +{ + "name": "mine-admin/app-store", + "description": "MineAdminアプリケーションマーケット可視化プラグイン", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "MineAdmin Team", + "role": "developer" + } + ], + "keywords": ["mineadmin", "app-store", "plugin-management"], + "homepage": "https://github.com/mineadmin/appstore", + "license": "MIT", + "composer": { + "require": { + "hyperf/async-queue": "^3.0" + }, + "psr-4": { + "Plugin\\MineAdmin\\AppStore\\": "src" + }, + "config": "Plugin\\MineAdmin\\AppStore\\ConfigProvider" + } +} +``` + +#### ConfigProvider 実装 +```php + [ + // 依存性注入設定 + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'commands' => [ + Command\AppStoreCommand::class, + ], + 'listeners' => [ + Listener\PluginEventListener::class, + ], + 'publish' => [ + [ + 'id' => 'appstore-config', + 'description' => 'App Store設定ファイル', + 'source' => __DIR__ . '/../publish/appstore.php', + 'destination' => BASE_PATH . '/config/autoload/appstore.php', + ], + ], + ]; + } +} +``` + +## 完全なプラグイン開発サンプル + +### 1. ユーザー管理プラグイン (ハイブリッド型) + +以下は完全なユーザー管理プラグインのサンプルで、フロントエンドとバックエンドを含むハイブリッド型プラグインの開発方法を示しています。 + +#### mine.json 設定 +```json +{ + "name": "mycompany/user-manager", + "description": "ユーザー管理プラグイン", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "composer": { + "require": { + "hyperf/database": "^3.0", + "hyperf/validation": "^3.0" + }, + "psr-4": { + "Plugin\\MyCompany\\UserManager\\": "src" + }, + "config": "Plugin\\MyCompany\\UserManager\\ConfigProvider" + }, + "package": { + "dependencies": { + "element-plus": "^2.4.0" + } + } +} +``` + +#### コアコントローラ実装 +```php +request->all(); + $result = $this->service->getPageList($params); + return $this->success($result); + } + + #[PostMapping('/user')] + public function create(): array + { + $data = $this->request->all(); + $user = $this->service->create($data); + return $this->success($user, 'ユーザー作成成功'); + } + + #[PutMapping('/user/{id}')] + public function update(int $id): array + { + $data = $this->request->all(); + $this->service->update($id, $data); + return $this->success(null, '更新成功'); + } + + #[DeleteMapping('/user/{id}')] + public function delete(int $id): array + { + $this->service->delete($id); + return $this->success(null, '削除成功'); + } +} +``` + +#### サービス層実装 +```php +where(function($q) use ($params) { + $q->where('username', 'like', "%{$params['keyword']}%") + ->orWhere('email', 'like', "%{$params['keyword']}%"); + }); + } + + $paginator = $query->paginate( + $params['pageSize'] ?? 15, + ['*'], + 'page', + $params['page'] ?? 1 + ); + + return [ + 'items' => $paginator->items(), + 'pageInfo' => [ + 'total' => $paginator->total(), + 'currentPage' => $paginator->currentPage(), + 'totalPage' => $paginator->lastPage() + ] + ]; + } + + public function create(array $data): User + { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + return User::create($data); + } + + public function update(int $id, array $data): bool + { + if (isset($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } + + return User::query()->where('id', $id)->update($data) > 0; + } + + public function delete(int $id): bool + { + return User::destroy($id) > 0; + } +} +``` + +### 2. バックエンド型プラグインサンプル - APIサービスプラグイン + +以下は純粋なバックエンドAPIサービスプラグインのサンプルです。 + +#### プラグイン設定 (mine.json) + +```json +{ + "name": "mycompany/api-service", + "description": "APIサービスプラグイン", + "version": "1.0.0", + "type": "backend", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "keywords": ["api", "service"], + "license": "MIT", + "composer": { + "require": { + "guzzlehttp/guzzle": "^7.0" + }, + "psr-4": { + "Plugin\\MyCompany\\ApiService\\": "src" + }, + "config": "Plugin\\MyCompany\\ApiService\\ConfigProvider" + } +} +``` + +#### ConfigProvider 実装 + +```php + [ + Contract\ApiClientInterface::class => Service\ApiClient::class, + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'commands' => [ + Command\ApiSyncCommand::class, + ], + 'publish' => [ + [ + 'id' => 'api-service-config', + 'description' => 'APIサービス設定ファイル', + 'source' => __DIR__ . '/../publish/api_service.php', + 'destination' => BASE_PATH . '/config/autoload/api_service.php', + ], + ], + ]; + } +} +``` + +### 3. フロントエンド型プラグインサンプル - データ可視化プラグイン + +以下は純粋なフロントエンドのデータ可視化プラグインのサンプルです。 + +#### mine.json 設定 +```json +{ + "name": "mycompany/data-visualization", + "description": "データ可視化プラグイン", + "version": "1.0.0", + "type": "frontend", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "package": { + "dependencies": { + "echarts": "^5.4.0", + "vue-echarts": "^6.5.0" + } + } +} +``` + +## 完全なプラグイン開発ベストプラクティス + +### 1. ディレクトリ構造規範 + +``` +plugin/vendor-name/plugin-name/ +├── mine.json # プラグイン設定ファイル +├── src/ # PHPバックエンドコード +│ ├── ConfigProvider.php # 設定プロバイダ +│ ├── Controller/ # コントローラ +│ ├── Service/ # サービス層 +│ ├── Model/ # モデル +│ ├── Command/ # コマンドライン +│ ├── Listener/ # イベントリスナー +│ └── Middleware/ # ミドルウェア +├── web/ # フロントエンドコード +│ ├── views/ # Vueページコンポーネント +│ ├── api/ # APIインターフェースカプセル化 +│ ├── components/ # 共通コンポーネント +│ └── locales/ # 国際化 +├── Database/ # データベース +│ ├── Migrations/ # マイグレーションファイル +│ └── Seeders/ # データシード +└── publish/ # 公開ファイル + └── config.php # 設定ファイル +``` + +### 2. 命名規範 + +- **プラグイン名**: `vendor/plugin-name` 形式を使用 +- **名前空間**: `Plugin\VendorName\PluginName` +- **クラス名**: 大文字キャメルケースを使用 +- **メソッド名**: 小文字キャメルケースを使用 + +### 3. コアコンポーネントサンプル + +#### コントローラサンプル + +```php +request->all(); + $result = $this->service->getList($params); + return $this->success($result); + } + + #[PostMapping('/create')] + public function create(): array + { + $data = $this->request->all(); + $result = $this->service->create($data); + return $this->success($result, '作成成功'); + } + $user = $this->userService->find($id); + + if (!$user) { + return $this->error('ユーザーが存在しません', 404); + } + + return $this->success($user); + } + + /** + * ユーザー更新 + */ + #[PutMapping('/users/{id:\d+}')] + public function update(int $id): array + { + $data = $this->request->all(); + + $user = $this->userService->update($id, $data); + + return $this->success($user, 'ユーザー更新成功'); + } + + /** + * ユーザー削除 + */ + #[DeleteMapping('/users/{id:\d+}')] + public function destroy(int $id): array + { + $this->userService->delete($id); + + return $this->success([], 'ユーザー削除成功'); + } + + /** + * ユーザー一括インポート + */ + #[PostMapping('/users/import')] + public function import(): array + { + $file = $this->request->file('file'); + + if (!$file || !$file->isValid()) { + return $this->error('有効なファイルをアップロードしてください'); + } + + $result = $this->userService->importFromFile($file); + + return $this->success($result, 'インポート完了'); + } + + /** + * ユーザーデータエクスポート + */ + #[GetMapping('/users/export')] + public function export(): array + { + $params = $this->request->all(); + $filePath = $this->userService->exportToFile($params); + + return $this->success(['file_path' => $filePath], 'エクスポート成功'); + } +} +``` + +#### 4. サービス層 (src/Service/UserService.php) + +```php +repository->getList($params); + } + + /** + * ユーザー作成 + */ + public function create(array $data): array + { + // パスワード暗号化 + if (isset($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } + + // ユーザーアバター生成 + if (!isset($data['avatar'])) { + $data['avatar'] = $this->generateAvatar($data['username']); + } + + $user = $this->repository->create($data); + + // ユーザー作成イベントトリガー + event(new UserCreatedEvent($user)); + + return $user->toArray(); + } + + /** + * ユーザー更新 + */ + public function update(int $id, array $data): array + { + // パスワード更新処理 + if (isset($data['password']) && !empty($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } else { + unset($data['password']); + } + + $user = $this->repository->update($id, $data); + + // ユーザー更新イベントトリガー + event(new UserUpdatedEvent($user)); + + return $user->toArray(); + } + + /** + * ファイルからユーザーインポート + */ + public function importFromFile($file): array + { + $filePath = $file->getPath() . '/' . $file->getFilename(); + + // Excelファイル読み取り + $data = $this->parseExcelFile($filePath); + + $successCount = 0; + $errorCount = 0; + $errors = []; + + foreach ($data as $index => $row) { + try { + $this->create([ + 'username' => $row['username'], + 'email' => $row['email'], + 'phone' => $row['phone'] ?? null, + 'password' => $row['password'] ?? '123456', + ]); + $successCount++; + } catch (\Exception $e) { + $errorCount++; + $errors[] = "{$index}行目: " . $e->getMessage(); + } + } + + return [ + 'success_count' => $successCount, + 'error_count' => $errorCount, + 'errors' => $errors + ]; + } + + /** + * ユーザーをファイルにエクスポート + */ + public function exportToFile(array $params = []): string + { + $users = $this->repository->getAllForExport($params); + + // Excelファイル生成 + $filePath = $this->generateExcelFile($users); + + return $filePath; + } + + /** + * ユーザーアバター生成 + */ + private function generateAvatar(string $username \ No newline at end of file diff --git a/docs/ja/plugin/guide.md b/docs/ja/plugin/guide.md new file mode 100644 index 0000000..bf11f0e --- /dev/null +++ b/docs/ja/plugin/guide.md @@ -0,0 +1,301 @@ +# クイックスタートガイド + +このガイドでは、最初のMineAdminプラグインを作成する手順を、環境準備からプラグイン公開までの完全な流れで説明します。 + +## 前提条件 + +開始前に、以下の準備を確認してください: + +1. **MineAdminのインストール**:MineAdminシステムが正常に動作していること +2. **技術スタックの理解**: + - PHP 8.1+ とHyperfフレームワーク + - Vue 3 + TypeScript (フロントエンド開発が必要な場合) + - Composerパッケージマネージャー + +## 環境設定 + +### 1. AccessTokenの取得 + +プラグインマーケットと開発者機能を使用するにはAccessTokenが必要です: + +1. [MineAdmin公式サイト](https://www.mineadmin.com/login)にログイン +2. [個人設定ページ](https://www.mineadmin.com/member/setting)に移動 +3. AccessTokenを確認してコピー + +### 2. 環境変数の設定 + +プロジェクトルートの`.env`ファイルに追加: + +```ini +# MineAdmin AccessToken +MINE_ACCESS_TOKEN=あなたのAccessToken +``` + +### 3. プラグインシステムの初期化 + +初めてプラグインシステムを使用する場合、初期化が必要です: + +```bash +# プラグイン拡張システムの初期化 (MineAdmin 3.0+ ではデフォルトで初期化済み) +php bin/hyperf.php mine-extension:initial +``` + +## 最初のプラグイン作成 + +### 1. コマンドラインでプラグイン作成 + +```bash +# 混合型プラグインを作成 +php bin/hyperf.php mine-extension:create mycompany/hello-world \ + --name "Hello World" \ + --type mixed \ + --author "あなたの名前" \ + --description "私の最初のMineAdminプラグイン" +``` + +**パラメータ説明**: +- `mycompany/hello-world`:プラグインパス (名前空間/プラグイン名) +- `--name`:プラグイン表示名 +- `--type`:プラグインタイプ (mixed/backend/frontend) +- `--author`:作者名 +- `--description`:プラグイン説明 + +### 2. 生成されるディレクトリ構造 + +コマンド実行後、`plugin/mycompany/hello-world/`ディレクトリに以下が生成されます: + +``` +plugin/mycompany/hello-world/ +├── mine.json # プラグイン設定ファイル +├── src/ # バックエンドソースディレクトリ +│ ├── ConfigProvider.php # 設定プロバイダー +│ ├── InstallScript.php # インストールスクリプト +│ └── UninstallScript.php # アンインストールスクリプト +├── web/ # フロントエンドソースディレクトリ +└── Database/ # データベース関連 + ├── Migrations/ # データベースマイグレーション + └── Seeders/ # データシーダー +``` + +## プラグインの開発 + +### 1. プラグイン情報の設定 + +`mine.json`ファイルを編集して情報を充実させます: + +```json +{ + "name": "mycompany/hello-world", + "description": "私の最初のMineAdminプラグイン", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "あなたの名前", + "role": "developer" + } + ], + "composer": { + "psr-4": { + "Plugin\\MyCompany\\HelloWorld\\": "src" + }, + "config": "Plugin\\MyCompany\\HelloWorld\\ConfigProvider" + } +} +``` + +### 2. 設定プロバイダーの実装 + +`src/ConfigProvider.php`を編集: + +```php + [ + // 依存性注入の設定 + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'publish' => [ + // 設定ファイル公開設定 + ], + ]; + } +} +``` + +### 3. ビジネスロジックの追加 + +コントローラー`src/Controller/HelloController.php`を作成: + +```php + 200, + 'message' => 'MineAdminプラグインからの挨拶です!', + 'data' => [ + 'plugin' => 'hello-world', + 'timestamp' => time() + ] + ]; + } +} +``` + +### 4. フロントエンド開発 (オプション) + +`web/`ディレクトリにフロントエンドコンポーネントを追加: + +```vue + + + + +``` + +## プラグインのインストールとテスト + +### 1. プラグインのインストール + +```bash +# システムにプラグインをインストール +php bin/hyperf.php mine-extension:install mycompany/hello-world --yes +``` + +### 2. 機能のテスト + +開発サーバーを起動してAPIをテスト: + +```bash +# サービスを起動 +php bin/hyperf.php start + +# APIをテスト (新しいターミナル) +curl http://localhost:9501/hello-world/greeting +``` + +### 3. インストール状態の確認 + +```bash +# ローカルにインストールされたプラグインを表示 +php bin/hyperf.php mine-extension:local-list +``` + +## プラグイン管理コマンド + +### よく使うコマンド一覧 + +```bash +# リモートプラグインリストを表示 +php bin/hyperf.php mine-extension:list + +# リモートプラグインをダウンロード +php bin/hyperf.php mine-extension:download --name plugin-name + +# ローカルプラグインをインストール +php bin/hyperf.php mine-extension:install plugin/path --yes + +# プラグインをアンインストール +php bin/hyperf.php mine-extension:uninstall plugin/path --yes + +# ローカルプラグインを表示 +php bin/hyperf.php mine-extension:local-list +``` + +## 開発デバッグのヒント + +### 1. ログデバッグ + +プラグイン内でHyperfのログシステムを使用: + +```php +use Hyperf\Logger\LoggerFactory; + +$logger = $container->get(LoggerFactory::class)->get('plugin'); +$logger->info('Hello Worldプラグインのデバッグ', ['data' => $someData]); +``` + +### 2. 設定のホットリロード + +開発中に設定を変更した後はサービスを再起動: + +```bash +# Hyperfサービスを再起動 +php bin/hyperf.php start +``` + +### 3. フロントエンドのホットリロード + +MineAdminフロントエンド開発環境を使用している場合: + +```bash +# フロントエンドプロジェクトディレクトリで +npm run dev +``` + +## 次のステップ + +最初のプラグインを作成しました!次は: + +1. [プラグイン構造の詳細](./structure.md) +2. [完全な開発フローの学習](./develop.md) +3. [ライフサイクル管理の理解](./lifecycle.md) +4. [その他の例を見る](./examples.md) + +## よくある質問 + +### Q: プラグインのインストールに失敗したら? +A: `mine.json`の設定が正しいか確認し、PSR-4オートロードパスが正しいことを確認してください。 + +### Q: プラグインをデバッグするには? +A: Hyperfのログシステムとデバッグツールを使用し、`runtime/logs/`ディレクトリのログファイルを確認します。 + +### Q: フロントエンドコンポーネントが表示されない? +A: フロントエンドファイルが`web/`ディレクトリにあることを確認し、プラグインインストール時にフロントエンドプロジェクトに自動コピーされます。 \ No newline at end of file diff --git a/docs/ja/plugin/index.md b/docs/ja/plugin/index.md index e8d30de..fddee74 100644 --- a/docs/ja/plugin/index.md +++ b/docs/ja/plugin/index.md @@ -1,49 +1,117 @@ -# 準備作業 +# MineAdmin プラグインシステム -::: tip -MineAdmin アプリケーションを開発するには、まず MineAdmin と Hyperf フレームワークに慣れる必要があり、その後以下の準備作業を行います。 -::: +MineAdmin プラグインシステムは強力な拡張機能を提供し、開発者が再利用可能な機能モジュールを作成し、システムのモジュール化と拡張性を実現できます。 -## AccessTokenの取得 +## プラグインシステムアーキテクチャ -MineAdminでは、プラグインアプリケーションのダウンロード、更新、または開発のいずれにおいても `ACCESS_TOKEN` が必要です。 +MineAdmin のプラグインシステムは Hyperf フレームワークの ConfigProvider メカニズムに基づいており、完全なプラグインライフサイクル管理と自動展開機能を提供します。 -取得手順: +```plantuml +@startuml +!define RECTANGLE class -- [MineAdmin](https://www.mineadmin.com/login) 公式サイトにログイン -- `個人センター` の [_設定_](https://www.mineadmin.com/member/setting) ページに移動 -- `私のAccessToken` をクリックして確認 +RECTANGLE "MineAdmin Core" as core { + + bin/hyperf.php + + Plugin::init() +} -::: danger +RECTANGLE "Plugin Management" as mgmt { + + App-Store Component + + Extension Commands + + Plugin Loader +} ---- - -注意 +RECTANGLE "Plugin Structure" as structure { + + mine.json (設定ファイル) + + src/ (バックエンドコード) + + web/ (フロントエンドコード) + + Database/ (データベース) +} -自分の AccessToken は厳重に管理し、絶対に漏らさないでください!!! +RECTANGLE "Official Plugins" as official { + + app-store +} ---- +core --> mgmt : プラグイン初期化 +mgmt --> structure : プラグイン読み込み +mgmt --> official : 公式プラグイン管理 +structure --> core : サービス登録 -::: +@enduml +``` + +## コアコンポーネント + +### 1. プラグインローダー +- **ファイル**: `bin/hyperf.php` ([GitHub](https://github.com/mineadmin/mineadmin/blob/master/bin/hyperf.php)) +- **原理**: `Plugin::init()` メソッドでアプリケーション起動時に自動的にすべてのインストール済みプラグインを読み込み +- **実装**: `plugin/` ディレクトリ内のすべてのプラグインをスキャンし、ConfigProvider を登録 + +### 2. App-Store コンポーネント +- **リポジトリ**: [mineadmin/appstore](https://github.com/mineadmin/appstore) +- **機能**: プラグインのダウンロード、インストール、アンインストール、更新などの管理機能を提供 +- **設定**: `ConfigProvider` を通じてサービスと設定を登録 + +### 3. プラグイン設定システム +- **コアファイル**: `mine.json` +- **原理**: プラグインのメタデータ、依存関係、インストールスクリプトなどの情報を定義 +- **読み込み**: プラグインインストール時に解析しシステムに登録 + +## 公式プラグイン + +MineAdmin はデフォルトで以下の公式プラグインを提供します: -## バックエンドの .env ファイル設定 +| プラグイン名 | 機能説明 | リポジトリURL | +|---------|----------|----------| +| app-store | アプリストア管理プラグイン、プラグインのダウンロード、インストール、アンインストール、更新などの管理機能を提供 | [GitHub](https://github.com/mineadmin/appstore) | -バックエンドのルートディレクトリにある _.env_ ファイルを開き、**MINE_ACCESS_TOKEN** 項目を見つけて、先ほどコピーした文字列を **等号** の後ろに貼り付けます。 +> 注:コードジェネレータ、スケジュールタスク管理などの他のプラグインはアプリストアから取得するか、独自に開発可能 -```ini [.env] -APP_NAME = MineAdmin +## プラグインタイプ -APP_ENV = dev +MineAdmin は3種類のプラグインをサポートしています: -# 省略... +### Mixed (混合型プラグイン) +フロントエンドとバックエンドの完全な機能を含むプラグインで、完全なビジネスモジュールを提供します。 -MINE_ACCESS_TOKEN = 107299501236086 +### Backend (バックエンドプラグイン) +バックエンドロジックのみを含むプラグインで、主にAPIサービスとビジネスロジックを提供します。 + +### Frontend (フロントエンドプラグイン) +フロントエンドインターフェースのみを含むプラグインで、主にユーザーインターフェースコンポーネントを提供します。 + +## クイックスタート + +### 環境準備 + +MineAdmin プラグインの開発には以下が必要です: + +1. **技術スタックの理解**:MineAdmin と Hyperf フレームワーク +2. **AccessToken の取得**: + - [MineAdmin 公式サイト](https://www.mineadmin.com/login)にログイン + - 個人センター → [設定ページ](https://www.mineadmin.com/member/setting)に移動 + - AccessToken を取得 + +3. **環境変数の設定**: +```ini +# .env ファイル +MINE_ACCESS_TOKEN=あなたのAccessToken ``` -## 開発者申請 +::: warning 注意 +AccessToken は適切に保管し、漏洩しないように注意してください! +::: + +### 開発者認証 -ローカルでアプリケーションを開発し、自分だけで使用する場合、開発者認証権限は必要ありません。また、他の人に配布することも可能です。 +- **ローカル開発**:認証不要、自由に開発と配布が可能 +- **アプリストア公開**:開発者認証が必要、MineAdmin チームに連絡して権限を開通 -公式アプリケーションマーケットに掲載する予定の場合、開発者認証を受けた後で初めてアプリケーションを公開でき、公式の著作権保護を受けられます。 +## 関連ドキュメント -現在、オンラインでの直接申請認証はサポートされていないため、**MineAdminチームメンバー** に連絡して開発者権限を開設してもらう必要があります。 \ No newline at end of file +- [クイックスタートガイド](./guide.md) - 最初のプラグイン作成 +- [開発ガイド](./develop.md) - 詳細な開発フロー +- [プラグイン構造](./structure.md) - ディレクトリ構造規約 +- [ライフサイクル管理](./lifecycle.md) - インストール/アンインストールフロー +- [API リファレンス](./api.md) - インターフェースドキュメント +- [サンプルコード](./examples.md) - 実践的なケーススタディ \ No newline at end of file diff --git a/docs/ja/plugin/lifecycle.md b/docs/ja/plugin/lifecycle.md new file mode 100644 index 0000000..4830ada --- /dev/null +++ b/docs/ja/plugin/lifecycle.md @@ -0,0 +1,544 @@ +# プラグインライフサイクル管理 + +MineAdminプラグインのライフサイクル管理について詳細に説明します。インストール、有効化、無効化、更新、アンインストールの完全なプロセスを含みます。 + +## ライフサイクル概要 + +MineAdminプラグインのライフサイクルには以下の段階があります: + +```plantuml +@startuml +!define RECTANGLE class + +state "未インストール" as uninstalled +state "ダウンロード済み" as downloaded +state "インストール済み" as installed +state "有効化済み" as enabled +state "無効化済み" as disabled +state "更新が必要" as needUpdate +state "アンインストール済み" as uninstalled2 + +[*] --> uninstalled +uninstalled --> downloaded : mine-extension:download +downloaded --> installed : mine-extension:install +installed --> enabled : 自動有効化 +enabled --> disabled : プラグイン無効化 +disabled --> enabled : プラグイン有効化 +enabled --> needUpdate : 新しいバージョンを検出 +needUpdate --> enabled : mine-extension:update +enabled --> uninstalled2 : mine-extension:uninstall +disabled --> uninstalled2 : mine-extension:uninstall +uninstalled2 --> [*] + +note right of installed : InstallScriptを実行 +note right of uninstalled2 : UninstallScriptを実行 + +@enduml +``` + +## プラグイン発見とロード + +### 1. プラグイン発見メカニズム + +**コア実装**: `Plugin::init()` メソッドが `bin/hyperf.php` ([GitHub](https://github.com/mineadmin/mineadmin/blob/master/bin/hyperf.php)) で呼び出される + +```plantuml +@startuml +participant "Application" as app +participant "Plugin::init()" as plugin +participant "ConfigProvider" as config +participant "Hyperf Container" as container + +app -> plugin : アプリ起動時に呼び出し +plugin -> plugin : plugin/ ディレクトリをスキャン +plugin -> plugin : mine.json 設定を読み込み +plugin -> config : ConfigProviderをロード +config -> container : コンテナにサービス登録 +container -> app : サービス利用可能 + +note right of plugin : インストール済みプラグインのみロード\n(install.lock ファイルが存在する場合) + +@enduml +``` + +### 2. ロードプロセス詳細 + +1. **プラグインディレクトリスキャン**: `plugin/` 配下の全サブディレクトリを走査 +2. **インストール状態確認**: `install.lock` ファイルの存在を確認 +3. **設定読み込み**: `mine.json` 設定ファイルを解析 +4. **ConfigProviderロード**: プラグインサービスをHyperfコンテナに登録 +5. **ルート登録**: コントローラールートを自動登録 +6. **ミドルウェアロード**: プラグインミドルウェアを登録 +7. **イベントリスナー登録**: イベントリスナーをロード + +## ダウンロード段階 + +### コマンド使用 + +```bash +# 指定プラグインをダウンロード +php bin/hyperf.php mine-extension:download --name plugin-name + +# ダウンロード可能なプラグイン一覧を表示 +php bin/hyperf.php mine-extension:list +``` + +### ダウンロードプロセス + +1. **AccessToken検証**: `MINE_ACCESS_TOKEN` 環境変数を確認 +2. **リモートリポジトリにリクエスト**: MineAdmin公式リポジトリからプラグイン情報を取得 +3. **プラグインパッケージダウンロード**: 一時ディレクトリにzipファイルをダウンロード +4. **ファイル解凍**: `plugin/vendor/plugin-name/` ディレクトリに展開 +5. **完全性検証**: `mine.json` ファイルの存在と形式を確認 + +### 実装原理 + +**コアサービス**: App-Storeコンポーネント ([GitHub](https://github.com/mineadmin/appstore)) がダウンロード機能を提供 + +```php +// 疑似コード例 +class DownloadService +{ + public function download(string $pluginName): bool + { + // 1. アクセストークン検証 + $this->validateAccessToken(); + + // 2. プラグイン情報取得 + $pluginInfo = $this->getPluginInfo($pluginName); + + // 3. プラグインパッケージダウンロード + $packagePath = $this->downloadPackage($pluginInfo['download_url']); + + // 4. ターゲットディレクトリに解凍 + $this->extractPackage($packagePath, $this->getPluginPath($pluginName)); + + return true; + } +} +``` + +## インストール段階 + +### コマンド使用 + +```bash +# プラグインをインストール +php bin/hyperf.php mine-extension:install vendor/plugin-name --yes + +# 強制再インストール +php bin/hyperf.php mine-extension:install vendor/plugin-name --force +``` + +### インストールフロー詳細 + +> ⚠️ **重要**: 設定ファイル公開、環境検出、データベースマイグレーションは `InstallScript` で処理し、ConfigProviderのpublish機能に依存しないこと。 + +```plantuml +@startuml +start + +:プラグインディレクトリ確認; +if (ディレクトリ存在?) then (はい) + :mine.json設定を読み込み; + if (設定有効?) then (はい) + :依存関係を確認; + if (依存満たす?) then (はい) + :Composer依存をインストール; + :フロントエンドファイルをコピー; + #pink:InstallScriptを実行; + note right + InstallScriptで処理: + - 環境検出 + - 設定ファイル公開 + - データベースマイグレーション + - 初期データ投入 + end note + if (InstallScript成功?) then (はい) + :install.lockを作成; + :プラグインリストに登録; + :インストールイベントをトリガー; + :キャッシュをクリア; + stop + else (失敗) + :操作をロールバック; + :一時ファイルをクリア; + stop + endif + else (満たさない) + :依存プラグインのインストールを促す; + stop + endif + else (無効) + :設定エラーを報告; + stop + endif +else (いいえ) + :プラグインが存在しないと報告; + stop +endif + +@enduml +``` + +### 1. 事前チェック + +```php +// インストール前チェックロジック +class InstallChecker +{ + public function check(string $pluginPath): array + { + $errors = []; + + // プラグインディレクトリ確認 + if (!is_dir($pluginPath)) { + $errors[] = 'プラグインディレクトリが存在しません'; + } + + // mine.json確認 + $configPath = $pluginPath . '/mine.json'; + if (!file_exists($configPath)) { + $errors[] = 'mine.json 設定ファイルが存在しません'; + } + + // 依存関係確認 + $config = json_decode(file_get_contents($configPath), true); + foreach ($config['require'] ?? [] as $dependency => $version) { + if (!$this->isDependencyMet($dependency, $version)) { + $errors[] = "依存 {$dependency} バージョン {$version} が満たされていません"; + } + } + + return $errors; + } +} +``` + +### 2. Composer依存インストール + +プラグインのComposer依存を処理: + +```json +// mine.json のcomposer設定 +{ + "composer": { + "require": { + "hyperf/async-queue": "^3.0", + "symfony/console": "^6.0" + }, + "psr-4": { + "Plugin\\Vendor\\PluginName\\": "src" + } + } +} +``` + +システムが自動実行: +```bash +composer require hyperf/async-queue:^3.0 symfony/console:^6.0 +``` + +### 3. InstallScript処理 ⭐ + +> **ベストプラクティス**: データベースマイグレーション、設定公開、環境検出は `InstallScript` で処理: + +```php +// InstallScriptですべてのインストールロジックを処理 +class InstallScript +{ + public function handle(): bool + { + // 1. 環境検出 + if (!$this->checkEnvironment()) { + echo "環境要件を満たしていません\n"; + return false; + } + + // 2. 設定ファイル公開(ConfigProviderのpublishを使用しない) + $this->publishConfig(); + + // 3. データベースマイグレーション実行 + if (!$this->runMigrations()) { + echo "データベースマイグレーション失敗\n"; + return false; + } + + // 4. データ初期化 + $this->seedData(); + + return true; + } + + private function publishConfig(): void + { + $source = __DIR__ . '/../publish/config/plugin.php'; + $target = BASE_PATH . '/config/autoload/plugin.php'; + + if (!file_exists($target)) { + copy($source, $target); + echo "設定ファイルが公開されました\n"; + } + } + + private function runMigrations(): bool + { + $migrationPath = __DIR__ . '/../Database/Migrations'; + + if (is_dir($migrationPath)) { + // Hyperfのマイグレーションコマンドを使用 + $container = \Hyperf\Context\ApplicationContext::getContainer(); + $application = $container->get(\Hyperf\Contract\ApplicationInterface::class); + + $input = new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'migrate', + '--path' => $migrationPath, + ]); + + $output = new \Symfony\Component\Console\Output\BufferedOutput(); + $exitCode = $application->run($input, $output); + + return $exitCode === 0; + } + + return true; + } +} +``` + +### 4. フロントエンドファイルコピー + +`web/` ディレクトリのファイルをフロントエンドプロジェクトにコピー: + +``` +plugin/vendor/plugin-name/web/ → フロントエンドプロジェクト対応ディレクトリ +├── views/example.vue → src/views/plugin/vendor/plugin-name/example.vue +├── components/ExampleComp.vue → src/components/plugin/vendor/plugin-name/ExampleComp.vue +└── api/example.js → src/api/plugin/vendor/plugin-name/example.js +``` + +### 5. 設定ファイル公開 ⚠️ + +> **注意**: ConfigProviderの `publish` 機能はプラグインシステムで信頼性が低いため、InstallScriptで手動処理: + +```php +// 非推奨:ConfigProviderのpublishは動作しない可能性あり +'publish' => [ + // プラグインではこの方法は実行されない可能性あり +] + +// 推奨:InstallScriptで手動公開 +protected function publishConfig(): void +{ + $configs = [ + [ + 'source' => __DIR__ . '/../publish/config/plugin.php', + 'target' => BASE_PATH . '/config/autoload/plugin.php', + ], + [ + 'source' => __DIR__ . '/../publish/config/routes.php', + 'target' => BASE_PATH . '/config/routes/plugin.php', + ], + ]; + + foreach ($configs as $config) { + if (!file_exists($config['target'])) { + copy($config['source'], $config['target']); + echo "設定ファイルが公開されました: {$config['target']}\n"; + } + } +} +``` + +### 6. インストールロックファイル作成 + +インストール成功時に `install.lock` ファイルを作成: + +``` +plugin/vendor/plugin-name/install.lock +``` + +ファイル内容にインストール情報を含む: +```json +{ + "installed_at": "2024-01-01 12:00:00", + "version": "1.0.0", + "installer": "admin", + "checksum": "abc123..." +} +``` + +## 有効化/無効化管理 + +### プラグイン状態制御 + +MineAdminはプラグインをアンインストールせずに一時無効化可能: + +```bash +# プラグインを無効化 +php bin/hyperf.php mine-extension:disable vendor/plugin-name + +# プラグインを有効化 +php bin/hyperf.php mine-extension:enable vendor/plugin-name + +# プラグイン状態を確認 +php bin/hyperf.php mine-extension:status vendor/plugin-name +``` + +### 状態管理メカニズム + +状態情報は `install.lock` ファイルに保存: + +```json +{ + "installed_at": "2024-01-01 12:00:00", + "version": "1.0.0", + "status": "enabled", // enabled | disabled + "disabled_at": null, + "disabled_reason": null +} +``` + +## 更新段階 + +### 更新チェック + +```bash +# プラグイン更新を確認 +php bin/hyperf.php mine-extension:check-updates + +# 指定プラグインを更新 +php bin/hyperf.php mine-extension:update vendor/plugin-name + +# 全プラグインを更新 +php bin/hyperf.php mine-extension:update-all +``` + +### 更新フロー + +```plantuml +@startuml +start + +:リモートバージョンを確認; +if (新バージョンあり?) then (はい) + :現在のプラグインをバックアップ; + :新バージョンをダウンロード; + :完全性を検証; + :更新前スクリプトを実行; + :プラグインファイルを置換; + :データベースマイグレーションを実行; + :設定ファイルを更新; + :更新後スクリプトを実行; + if (更新成功?) then (はい) + :バージョン情報を更新; + :バックアップをクリア; + :更新イベントをトリガー; + stop + else (失敗) + :バックアップから復元; + :エラーを報告; + stop + endif +else (いいえ) + :更新不要; + stop +endif + +@enduml +``` + +### バージョン互換性処理 + +更新時にバージョン互換性を確認: + +```php +class UpdateManager +{ + public function checkCompatibility(string $currentVersion, string $newVersion): bool + { + // メジャーバージョンの互換性確認 + $current = $this->parseVersion($currentVersion); + $new = $this->parseVersion($newVersion); + + // メジャーバージョンが異なる場合は破壊的変更の可能性 + if ($current['major'] !== $new['major']) { + return $this->checkBreakingChanges($currentVersion, $newVersion); + } + + return true; + } +} +``` + +## アンインストール段階 + +### コマンド使用 + +```bash +# プラグインをアンインストール +php bin/hyperf.php mine-extension:uninstall vendor/plugin-name --yes + +# 強制アンインストール (エラーを無視) +php bin/hyperf.php mine-extension:uninstall vendor/plugin-name --force +``` + +### アンインストールフロー + +```plantuml +@startuml +start + +:プラグイン状態を確認; +if (プラグインがインストール済み?) then (はい) + :依存関係を確認; + if (他のプラグインが依存?) then (はい) + :依存衝突を通知; + if (強制アンインストール?) then (はい) + :アンインストールを継続; + else (いいえ) + :アンインストールをキャンセル; + stop + endif + endif + + :UninstallScriptを実行; + :データベーステーブルを削除; + :設定ファイルをクリア; + :フロントエンドファイルを削除; + :キャッシュをクリア; + :Composer依存を削除; + :プラグインディレクトリを削除; + :登録情報をクリア; + :アンインストールイベントをトリガー; + stop +else (いいえ) + :プラグインが未インストール; + stop +endif + +@enduml +``` + +### アンインストールスクリプト実行 + +```php +// UninstallScript例 +class UninstallScript +{ + public function handle(): bool + { + try { + // 1. データベースクリーンアップ + $this->cleanDatabase(); + + // 2. 設定ファイルクリーンアップ + $this->cleanConfigFiles(); + + // 3. キャッシュデータクリーンアップ + $this->cleanCache(); + + // 4. ログファイルクリーンアップ + $this->cleanLogs(); + + // 5. カスタムクリーンアップロジック \ No newline at end of file diff --git a/docs/ja/plugin/structure.md b/docs/ja/plugin/structure.md index 65b408d..7aa02b1 100644 --- a/docs/ja/plugin/structure.md +++ b/docs/ja/plugin/structure.md @@ -1,20 +1,462 @@ # プラグインのディレクトリ構造 -標準的なプラグインのディレクトリ構造についての説明 - ---- - -[プラグイン作成の章](./create.md)を例として - -```shell -- plugin/test/demo プラグインのルートディレクトリ --- plugin/test/demo/src プラグインのバックエンドディレクトリ ---- plugin/test/demo/src/InstallScript.php プラグインインストール時に実行されるクラスメソッド ---- plugin/test/demo/src/UninstallScript.php プラグインアンインストール時に実行されるクラスメソッド ---- plugin/test/demo/src/ConfigProvider.php プラグイン設定ディレクトリ、このファイルはhyperf公式の設定と一致 --- plugin/test/demo/Database プラグインのデータベースマイグレーションとシードファイルディレクトリ ---- plugin/test/demo/Database/Migrations プラグインのデータベースマイグレーションファイル ---- plugin/test/demo/Database/Seeder プラグインのデータベースシードファイル --- plugin/test/demo/web プラグインのフロントエンドディレクトリ --- plugin/test/demo/mine.json プラグインのコア情報ファイル -``` \ No newline at end of file +MineAdminプラグインの標準的なディレクトリ構造、ファイル規約、組織方法について詳しく説明します。 + +## 標準ディレクトリ構造 + +完全なMineAdminプラグインのディレクトリ構造は以下の通りです: + +``` +plugin/vendor/plugin-name/ # プラグインルートディレクトリ +├── mine.json # プラグインコア設定ファイル ⭐ +├── README.md # プラグイン説明ドキュメント +├── LICENSE # ライセンスファイル +├── composer.json # Composer依存設定 (オプション) +├── src/ # バックエンドソースディレクトリ ⭐ +│ ├── ConfigProvider.php # 設定プロバイダ ⭐ +│ ├── InstallScript.php # インストールスクリプト ⭐ +│ ├── UninstallScript.php # アンインストールスクリプト ⭐ +│ ├── Controller/ # コントローラディレクトリ +│ │ ├── AdminController.php # 管理者コントローラ +│ │ └── ApiController.php # APIコントローラ +│ ├── Service/ # サービス層ディレクトリ +│ │ └── ExampleService.php # ビジネスサービスクラス +│ ├── Repository/ # リポジトリ層ディレクトリ +│ │ └── ExampleRepository.php # データリポジトリクラス +│ ├── Model/ # モデルディレクトリ +│ │ └── Example.php # データモデル +│ ├── Request/ # リクエスト検証ディレクトリ +│ │ ├── CreateRequest.php # 作成リクエスト検証 +│ │ └── UpdateRequest.php # 更新リクエスト検証 +│ ├── Resource/ # リソース変換ディレクトリ +│ │ └── ExampleResource.php # リソース変換クラス +│ ├── Middleware/ # ミドルウェアディレクトリ +│ │ └── ExampleMiddleware.php # カスタムミドルウェア +│ ├── Command/ # コマンドラインディレクトリ +│ │ └── ExampleCommand.php # カスタムコマンド +│ ├── Listener/ # イベントリスナーディレクトリ +│ │ └── ExampleListener.php # イベントリスナー +│ └── Exception/ # 例外処理ディレクトリ +│ └── ExampleException.php # カスタム例外 +├── web/ # フロントエンドソースディレクトリ ⭐ +│ ├── views/ # ページコンポーネントディレクトリ +│ │ ├── index.vue # メインページ +│ │ ├── list.vue # リストページ +│ │ └── form.vue # フォームページ +│ ├── components/ # 共通コンポーネントディレクトリ +│ │ └── ExampleComponent.vue # 汎用コンポーネント +│ ├── api/ # APIインターフェースディレクトリ +│ │ └── example.js # インターフェース定義 +│ ├── router/ # ルーティング設定ディレクトリ +│ │ └── index.js # ルーティング設定 +│ ├── store/ # 状態管理ディレクトリ +│ │ └── example.js # 状態管理 +│ └── assets/ # 静的リソースディレクトリ +│ ├── images/ # 画像リソース +│ └── styles/ # スタイルファイル +├── Database/ # データベース関連ディレクトリ ⭐ +│ ├── Migrations/ # データベースマイグレーションファイル +│ │ └── 2024_01_01_000000_create_example_table.php +│ └── Seeders/ # データシードファイル +│ └── ExampleSeeder.php # データシードクラス +├── config/ # 設定ファイルディレクトリ +│ └── example.php # プラグイン設定ファイル +├── publish/ # 公開ファイルディレクトリ +│ ├── config/ # 設定ファイルテンプレート +│ │ └── example.php # 設定ファイルテンプレート +│ └── assets/ # 静的リソーステンプレート +├── tests/ # テストファイルディレクトリ +│ ├── Unit/ # ユニットテスト +│ ├── Feature/ # 機能テスト +│ └── TestCase.php # テスト基底クラス +├── docs/ # ドキュメントディレクトリ +│ ├── installation.md # インストールドキュメント +│ ├── usage.md # 使用ドキュメント +│ └── api.md # APIドキュメント +└── .gitignore # Git無視ファイル +``` + +## コアファイルの詳細 + +### 1. mine.json (プラグイン設定ファイル) + +**ファイルパス**: `mine.json` ([設定詳細](./mineJson.md)) + +プラグインのコア設定ファイルで、プラグインの基本情報、依存関係、ロード設定を定義します: + +```json +{ + "name": "vendor/plugin-name", + "description": "プラグイン説明", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "Author Name", + "email": "author@example.com", + "role": "developer" + } + ], + "keywords": ["mineadmin", "plugin"], + "homepage": "https://github.com/vendor/plugin-name", + "license": "MIT", + "require": { + "php": ">=8.1", + "hyperf/framework": "^3.0" + }, + "package": { + "dependencies": { + "vue": "^3.0", + "element-plus": "^2.0" + } + }, + "composer": { + "require": { + "hyperf/async-queue": "^3.0" + }, + "psr-4": { + "Plugin\\Vendor\\PluginName\\": "src" + }, + "config": "Plugin\\Vendor\\PluginName\\ConfigProvider" + } +} +``` + +### 2. ConfigProvider.php (設定プロバイダ) + +**ファイルパス**: `src/ConfigProvider.php` +**実装原理**: Hyperf ConfigProviderメカニズムに基づく ([GitHub](https://github.com/hyperf/hyperf/blob/master/src/config-provider/src/ConfigProvider.php)) + +> ⚠️ **注意**: ConfigProviderの`publish`機能はプラグインシステムで問題があるため、InstallScriptで設定ファイルの公開を処理することを推奨します。 + +```php + [], + 'annotations' => [ + 'scan' => [ + 'paths' => [__DIR__], + ], + ], + 'commands' => [], + 'listeners' => [], + // プラグインではpublish機能は推奨されません + // InstallScriptで設定ファイルの公開を処理してください + ]; + } +} +``` + +### 3. InstallScript.php (インストールスクリプト) ⭐ + +**ファイルパス**: `src/InstallScript.php` +**呼び出しタイミング**: `mine-extension:install`コマンド実行時 +**重要性**: 設定公開、環境検出、データベースマイグレーションをここで処理することを推奨 + +```php +checkEnvironment()) { + echo "環境チェック失敗\n"; + return false; + } + + // 2. 設定ファイルの公開 + $this->publishConfig(); + + // 3. データベースマイグレーションの実行 + $this->runMigrations(); + + // 4. データの初期化 + $this->seedData(); + + echo "プラグインインストール成功\n"; + return true; + } + + protected function checkEnvironment(): bool + { + // PHPバージョンのチェック + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + echo "PHPバージョンは >= 8.1が必要です\n"; + return false; + } + + // 必要な拡張のチェック + $requiredExtensions = ['redis', 'pdo', 'json']; + foreach ($requiredExtensions as $ext) { + if (!extension_loaded($ext)) { + echo "PHP拡張が不足しています: {$ext}\n"; + return false; + } + } + + return true; + } + + protected function publishConfig(): void + { + $source = __DIR__ . '/../publish/config/plugin.php'; + $target = BASE_PATH . '/config/autoload/plugin.php'; + + if (!file_exists($target)) { + copy($source, $target); + echo "設定ファイルが公開されました: {$target}\n"; + } + } + + protected function runMigrations(): void + { + $migrationPath = __DIR__ . '/../Database/Migrations'; + + if (is_dir($migrationPath)) { + // マイグレーションコマンドの実行 + $container = \Hyperf\Context\ApplicationContext::getContainer(); + $application = $container->get(ApplicationInterface::class); + $application->setAutoExit(false); + + $input = new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'migrate', + '--path' => $migrationPath, + ]); + + $output = new \Symfony\Component\Console\Output\BufferedOutput(); + $application->run($input, $output); + + echo "データベースマイグレーション完了\n"; + } + } + + protected function seedData(): void + { + // デフォルトデータの初期化 + // 例: デフォルト設定、メニューの作成など + } +} +``` + +### 4. UninstallScript.php (アンインストールスクリプト) ⭐ + +**ファイルパス**: `src/UninstallScript.php` +**呼び出しタイミング**: `mine-extension:uninstall`コマンド実行時 +**重要性**: 設定ファイル、データテーブル、関連リソースのクリーンアップ + +```php +backupData(); + + // 2. データベーステーブルの削除 + $this->dropTables(); + + // 3. 設定ファイルのクリーンアップ + $this->removeConfig(); + + // 4. キャッシュのクリア + $this->clearCache(); + + echo "プラグインアンインストール完了\n"; + return true; + } + + protected function backupData(): void + { + // 重要なデータを指定ディレクトリにバックアップ + $backupPath = BASE_PATH . '/runtime/backup/plugin_' . date('YmdHis') . '.sql'; + // バックアップロジックの実装 + } + + protected function dropTables(): void + { + // プラグインが作成したデータテーブルの削除 + $tables = ['plugin_example_table', 'plugin_settings']; + + foreach ($tables as $table) { + if (Db::schema()->hasTable($table)) { + Db::schema()->drop($table); + echo "データテーブルが削除されました: {$table}\n"; + } + } + } + + protected function removeConfig(): void + { + $configFile = BASE_PATH . '/config/autoload/plugin.php'; + + if (file_exists($configFile)) { + unlink($configFile); + echo "設定ファイルが削除されました: {$configFile}\n"; + } + } + + protected function clearCache(): void + { + // プラグイン関連キャッシュのクリア + $redis = \Hyperf\Context\ApplicationContext::getContainer() + ->get(\Hyperf\Redis\Redis::class); + + $redis->del('plugin:cache:*'); + echo "キャッシュがクリアされました\n"; + } +} +``` + +## ディレクトリ構造図解 + +```plantuml +@startuml +!define FOLDER rectangle +!define FILE rectangle + +FOLDER "Plugin Root" as root { + FILE "mine.json" as config + FOLDER "src/" as src { + FILE "ConfigProvider.php" as provider + FILE "InstallScript.php" as install + FILE "UninstallScript.php" as uninstall + FOLDER "Controller/" as controller + FOLDER "Service/" as service + FOLDER "Model/" as model + } + FOLDER "web/" as web { + FOLDER "views/" as views + FOLDER "components/" as components + FOLDER "api/" as api + } + FOLDER "Database/" as database { + FOLDER "Migrations/" as migrations + FOLDER "Seeders/" as seeders + } +} + +config --> provider : 設定ロード +provider --> install : インストール時に呼び出し +provider --> uninstall : アンインストール時に呼び出し +web --> views : フロントエンドページ +database --> migrations : データベース構造 +database --> seeders : 初期データ + +@enduml +``` + +## 異なるタイプのプラグインの構造の違い + +### Mixed (混合型プラグイン) +完全な`src/`と`web/`ディレクトリを含み、フロントエンドとバックエンドの完全な機能を提供します。 + +### Backend (バックエンドプラグイン) +`src/`ディレクトリのみを含み、APIサービスとビジネスロジックに焦点を当てます: + +``` +plugin/vendor/backend-plugin/ +├── mine.json +├── src/ +│ ├── ConfigProvider.php +│ ├── Controller/ +│ ├── Service/ +│ └── Model/ +└── Database/ +``` + +### Frontend (フロントエンドプラグイン) +`web/`ディレクトリのみを含み、フロントエンドインターフェースとインタラクションに焦点を当てます: + +``` +plugin/vendor/frontend-plugin/ +├── mine.json +├── web/ +│ ├── views/ +│ ├── components/ +│ └── assets/ +└── src/ + └── ConfigProvider.php # 最小設定 +``` + +## 命名規則 + +### 1. ディレクトリ命名 +- 小文字とハイフンを使用:`user-management` +- アンダースコアとスペースは避ける + +### 2. ファイル命名 +- PHPクラスファイルはPascalCase:`UserController.php` +- VueコンポーネントはPascalCase:`UserList.vue` +- 設定ファイルは小文字:`user.php` + +### 3. 名前空間規則 +PSR-4オートロード標準に従います: + +```php +// プラグインパス: plugin/mineadmin/user-manager/ +// 名前空間: Plugin\MineAdmin\UserManager\ +namespace Plugin\MineAdmin\UserManager\Controller; +``` + +## ファイル権限とセキュリティ + +### 1. ファイル権限設定 +```bash +# 適切なファイル権限を設定 +find plugin/ -type f -name "*.php" -exec chmod 644 {} \; +find plugin/ -type d -exec chmod 755 {} \; +``` + +### 2. セキュリティ注意事項 +- 機密設定には環境変数を使用 +- コード内でのキーのハードコーディングを避ける +- ユーザー入力を検証・フィルタリング +- 機密データの転送にはHTTPSを使用 + +## ベストプラクティス + +### 1. ファイル組織 +- 機能モジュールごとにコードを整理 +- ディレクトリ構造を明確に保つ +- 意味のあるファイル名を使用 + +### 2. コード規約 +- PSR-12コーディング標準に従う +- 適切なコメントを追加 +- 型宣言を使用 + +### 3. バージョン管理 +- `.gitignore`で不要なファイルを除外 +- 明確なコミットメッセージを作成 +- セマンティックバージョニングを使用 + +## サンプルプロジェクト構造 + +公式プラグインの実際の構造を参照: + +**App-Storeプラグイン**: MineAdmin公式アプリケーションマーケットプラグイン、標準的な混合型プラグイン構造を示す + +## よくある質問 + +### Q: プラグインディレクトリはどこに配置すべきですか? +A: プロジェクトルートの`plugin/` \ No newline at end of file diff --git a/docs/zh-hk/plugin/api.md b/docs/zh-hk/plugin/api.md new file mode 100644 index 0000000..d08ca74 --- /dev/null +++ b/docs/zh-hk/plugin/api.md @@ -0,0 +1,753 @@ +# API 參考文檔 + +本文檔詳細介紹 MineAdmin 插件系統的所有 API 接口、命令行工具和核心類庫。 + +## 命令行 API + +### 插件管理命令 + +#### 1. mine-extension:initial + +初始化插件擴展系統。 + +```bash +php bin/hyperf.php mine-extension:initial +``` + +**功能**: +- 發佈 app-store 配置文件 +- 初始化插件系統配置 +- 創建必要的目錄結構 + +**實現類**: `Mine\AppStore\Command\InitialCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/InitialCommand.php)) + +#### 2. mine-extension:list + +查詢遠程插件列表。 + +```bash +php bin/hyperf.php mine-extension:list [options] +``` + +**參數**: +| 參數 | 類型 | 默認值 | 説明 | +|------|------|--------|------| +| --type | string | all | 篩選擴展類型 (mixed/backend/frontend) | +| --name | string | - | 篩選擴展名稱 | +| --category | string | - | 篩選分類 | +| --author | string | - | 篩選作者 | + +**示例**: +```bash +# 查看所有插件 +php bin/hyperf.php mine-extension:list + +# 查看混合型插件 +php bin/hyperf.php mine-extension:list --type=mixed + +# 搜索特定插件 +php bin/hyperf.php mine-extension:list --name=user-manager +``` + +**實現類**: `Mine\AppStore\Command\ListCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/ListCommand.php)) + +#### 3. mine-extension:local-list + +查詢本地所有插件。 + +```bash +php bin/hyperf.php mine-extension:local-list [options] +``` + +**參數**: +| 參數 | 類型 | 默認值 | 説明 | +|------|------|--------|------| +| --status | string | all | 篩選狀態 (installed/enabled/disabled) | +| --type | string | all | 篩選類型 | + +**實現類**: `Mine\AppStore\Command\LocalListCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/LocalListCommand.php)) + +#### 4. mine-extension:download + +下載遠程插件到本地。 + +```bash +php bin/hyperf.php mine-extension:download --name=plugin-name [options] +``` + +**參數**: +| 參數 | 類型 | 必選 | 説明 | +|------|------|------|------| +| --name | string | 是 | 插件名稱 | +| --version | string | 否 | 指定版本 | +| --force | bool | 否 | 強制覆蓋已存在的插件 | + +**實現類**: `Mine\AppStore\Command\DownloadCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/DownloadCommand.php)) + +#### 5. mine-extension:install + +安裝指定插件。 + +```bash +php bin/hyperf.php mine-extension:install {path} [options] +``` + +**參數**: +| 參數 | 類型 | 必選 | 説明 | +|------|------|------|------| +| path | string | 是 | 插件路徑 (vendor/plugin-name) | +| --yes | bool | 否 | 跳過確認提示 | +| --force | bool | 否 | 強制重新安裝 | +| --skip-dependencies | bool | 否 | 跳過依賴檢查 | + +**示例**: +```bash +# 安裝插件 +php bin/hyperf.php mine-extension:install mineadmin/user-manager --yes + +# 強制重新安裝 +php bin/hyperf.php mine-extension:install mineadmin/user-manager --force +``` + +**實現類**: `Mine\AppStore\Command\InstallCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/InstallCommand.php)) + +#### 6. mine-extension:uninstall + +卸載指定插件。 + +```bash +php bin/hyperf.php mine-extension:uninstall {path} [options] +``` + +**參數**: +| 參數 | 類型 | 必選 | 説明 | +|------|------|------|------| +| path | string | 是 | 插件路徑 | +| --yes | bool | 否 | 跳過確認提示 | +| --force | bool | 否 | 強制卸載 (忽略錯誤) | +| --keep-data | bool | 否 | 保留用户數據 | + +**實現類**: `Mine\AppStore\Command\UninstallCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/UninstallCommand.php)) + +#### 7. mine-extension:create + +創建新插件。 + +```bash +php bin/hyperf.php mine-extension:create {path} [options] +``` + +**參數**: +| 參數 | 類型 | 默認值 | 説明 | +|------|------|--------|------| +| path | string | - | 插件路徑 (vendor/plugin-name) | +| --name | string | example | 插件顯示名稱 | +| --type | string | mixed | 插件類型 (mixed/backend/frontend) | +| --author | string | - | 作者名稱 | +| --description | string | - | 插件描述 | +| --license | string | MIT | 許可證類型 | + +**示例**: +```bash +php bin/hyperf.php mine-extension:create mycompany/hello-world \ + --name "Hello World" \ + --type mixed \ + --author "Your Name" \ + --description "我的第一個插件" +``` + +**實現類**: `Mine\AppStore\Command\CreateCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/CreateCommand.php)) + +## 核心類庫 API + +### Plugin 類 + +**文件位置**: `Mine\AppStore\Plugin` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Plugin.php)) + +插件系統的核心類,負責插件的加載和管理。 + +#### Plugin::init() + +初始化插件系統,在應用啓動時調用。 + +```php + [ + 'name' => 'vendor/plugin-name', + 'version' => '1.0.0', + 'path' => '/path/to/plugin', + 'config' => [...], // mine.json 配置 + 'status' => 'enabled' + ] +] +``` + +#### Plugin::isInstalled() + +檢查插件是否已安裝。 + +```php +install('vendor/plugin-name', [ + 'force' => false, + 'skip_dependencies' => false +]); + +if ($result['success']) { + echo "安裝成功"; +} else { + echo "安裝失敗: " . $result['message']; +} +``` + +#### uninstall() + +卸載插件。 + +```php +uninstall('vendor/plugin-name', [ + 'force' => false, + 'keep_data' => false +]); +``` + +#### update() + +更新插件。 + +```php +update('vendor/plugin-name'); +``` + +### ConfigProvider 基類 + +所有插件的 ConfigProvider 都應該遵循以下接口: + +```php + [ + InterfaceA::class => ImplementationA::class, + ], + + // 註解掃描路徑 + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + + // 命令行命令 + 'commands' => [ + CustomCommand::class, + ], + + // 事件監聽器 + 'listeners' => [ + CustomListener::class, + ], + + // 中間件 + 'middlewares' => [ + 'http' => [ + CustomMiddleware::class, + ], + ], + + // 配置文件發佈 + 'publish' => [ + [ + 'id' => 'config-id', + 'description' => '配置文件描述', + 'source' => __DIR__ . '/../publish/config.php', + 'destination' => BASE_PATH . '/config/autoload/plugin.php', + ], + ], + + // 進程配置 + 'processes' => [ + CustomProcess::class, + ], + ]; + } +} +``` + +## HTTP API + +### 插件管理接口 + +#### 獲取插件列表 + +```http +GET /admin/plugin/list +``` + +**請求參數**: +```json +{ + "page": 1, + "pageSize": 15, + "type": "mixed", + "status": "enabled", + "keyword": "search term" +} +``` + +**響應示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "list": [ + { + "name": "vendor/plugin-name", + "display_name": "插件顯示名稱", + "version": "1.0.0", + "description": "插件描述", + "author": "作者名稱", + "type": "mixed", + "status": "enabled", + "installed_at": "2024-01-01 12:00:00", + "updated_at": "2024-01-15 10:30:00" + } + ], + "total": 1 + } +} +``` + +#### 安裝插件 + +```http +POST /admin/plugin/install +``` + +**請求參數**: +```json +{ + "name": "vendor/plugin-name", + "version": "1.0.0", + "force": false +} +``` + +**響應示例**: +```json +{ + "code": 200, + "message": "安裝成功", + "data": { + "plugin": "vendor/plugin-name", + "version": "1.0.0", + "installed_at": "2024-01-01 12:00:00" + } +} +``` + +#### 卸載插件 + +```http +DELETE /admin/plugin/uninstall +``` + +**請求參數**: +```json +{ + "name": "vendor/plugin-name", + "keep_data": false +} +``` + +#### 啓用/禁用插件 + +```http +PUT /admin/plugin/toggle-status +``` + +**請求參數**: +```json +{ + "name": "vendor/plugin-name", + "status": "enabled" // enabled | disabled +} +``` + +## 事件 API + +### 插件事件系統 + +插件系統提供了豐富的事件鈎子,允許開發者在插件生命週期的關鍵節點執行自定義邏輯。 + +#### 事件類型 + +```php +success) { + // 插件安裝成功後的處理 + $this->clearCache(); + $this->sendNotification($event->pluginName); + $this->updateStatistics($event->pluginName); + } else { + // 安裝失敗的處理 + logger()->error('插件安裝失敗', [ + 'plugin' => $event->pluginName, + 'error' => $event->error + ]); + } + } +} +``` + +## 鈎子 API + +### 插件鈎子系統 + +MineAdmin 提供了鈎子系統,允許插件在系統關鍵點注入自定義邏輯。 + +#### 註冊鈎子 + +```php +info('用户嘗試登錄', ['user_id' => $user->id]); + }); + + HookManager::register('user.login.after', function($user) { + // 用户登錄後的處理邏輯 + $this->recordLoginHistory($user); + }); + + return [ + // ... 其他配置 + ]; + } +} +``` + +#### 觸發鈎子 + +```php +authenticate($credentials); + + if ($result) { + // 登錄後鈎子 + HookManager::trigger('user.login.after', $user); + } + + return $result; + } +} +``` + +#### 可用鈎子列表 + +| 鈎子名稱 | 觸發時機 | 參數 | +|----------|----------|------| +| `user.login.before` | 用户登錄前 | User $user | +| `user.login.after` | 用户登錄後 | User $user | +| `user.logout.before` | 用户退出前 | User $user | +| `user.logout.after` | 用户退出後 | User $user | +| `menu.render.before` | 菜單渲染前 | array $menus | +| `menu.render.after` | 菜單渲染後 | array $menus | +| `permission.check.before` | 權限檢查前 | string $permission, User $user | +| `permission.check.after` | 權限檢查後 | bool $result, string $permission, User $user | + +## 工具類 API + +### PluginHelper 類 + +提供插件開發的常用工具方法。 + +```php + config +config --> provider +provider --> backend +provider --> frontend +backend --> script +frontend --> script +script --> test +test --> publish + +note right of create : mine-extension:create +note right of config : 插件元數據配置 +note right of provider : 註冊服務和路由 +note right of backend : Controller + Service +note right of frontend : Vue3 + TypeScript +note right of script : InstallScript/UninstallScript +note right of test : 本地安裝測試 +note right of publish : 發佈到應用市場 + +@enduml +``` + +## 插件結構規範 + +基於 `app-store` 和 `code-generator` 插件的實際代碼,MineAdmin 插件有兩種典型結構: + +### 簡單插件結構(適合純後端或簡單功能) + +``` +plugin/mine-admin/plugin-name/ +├── mine.json # 插件配置文件 +├── install.lock # 安裝標記(自動生成) +└── src/ + ├── ConfigProvider.php # 配置提供者 + ├── Controller/ # 控制器 + │ └── IndexController.php + └── Service/ # 服務層 + └── Service.php +``` + +### 完整插件結構(適合複雜業務) + +``` +plugin/mine-admin/plugin-name/ +├── mine.json # 插件配置文件 +├── install.lock # 安裝標記(自動生成) +├── README.md # 插件説明 +├── src/ # 後端代碼 +│ ├── ConfigProvider.php # 配置提供者 +│ ├── InstallScript.php # 安裝腳本 +│ ├── UninstallScript.php # 卸載腳本 +│ ├── Http/ +│ │ ├── Controller/ # 控制器 +│ │ ├── Request/ # 請求驗證 +│ │ └── Vo/ # 值對象 +│ ├── Model/ # 數據模型 +│ ├── Repository/ # 倉儲層 +│ └── Service/ # 服務層 +├── web/ # 前端代碼 +│ ├── index.ts # 插件入口 +│ ├── api/ # API接口 +│ ├── views/ # Vue組件 +│ └── locales/ # 語言包 +├── Database/ # 數據庫 +│ ├── Migrations/ # 遷移文件 +│ └── Seeder/ # 種子數據 +├── languages/ # 後端語言包 +│ └── zh_CN/ +└── publish/ # 發佈資源 + └── template/ # 模板文件 +``` + +## 後端開發 + +### 1. ConfigProvider 配置提供者 + +基於 app-store 插件的實際實現: + +```php + [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + // 依賴注入(可選) + 'dependencies' => [ + // Interface::class => Implementation::class + ], + // 命令行(可選) + 'commands' => [ + // Command::class + ], + // 中間件(可選) + 'middlewares' => [ + 'http' => [ + // Middleware::class + ], + ], + // 事件監聽器(可選) + 'listeners' => [ + // Listener::class + ], + ]; + } +} +``` + +### 2. 控制器開發 + +參考 app-store 的 IndexController 實現: + +```php +success( + $this->service->getAppList($this->request->all()) + ); + } + + /** + * 下載插件 + */ + #[PostMapping("download")] + #[Permission("plugin:store:download")] + public function download(): ResponseInterface + { + $params = $this->request->all(); + $this->service->download($params); + return $this->success(); + } + + /** + * 安裝插件 + */ + #[PostMapping("install")] + #[Permission("plugin:store:install")] + public function install(): ResponseInterface + { + $params = $this->request->all(); + $this->service->install($params); + return $this->success(); + } + + /** + * 卸載插件 + */ + #[PostMapping("unInstall")] + #[Permission("plugin:store:uninstall")] + public function unInstall(): ResponseInterface + { + $params = $this->request->all(); + $this->service->unInstall($params); + return $this->success(); + } + + /** + * 本地插件安裝列表 + */ + #[GetMapping("getInstallList")] + #[RemoteState] + public function getInstallList(): ResponseInterface + { + return $this->success( + $this->service->getLocalAppInstallList() + ); + } + + /** + * 本地上傳安裝 + */ + #[PostMapping("uploadInstall")] + #[Permission("plugin:store:uploadInstall")] + public function uploadInstall(): ResponseInterface + { + return $this->success( + $this->service->uploadLocalApp($this->request->all()) + ); + } +} +``` + +**關鍵註解説明**: +- `#[Controller]`: 定義控制器路由前綴 +- `#[Auth]`: 需要登錄驗證 +- `#[Permission]`: 權限驗證 +- `#[GetMapping]`/`#[PostMapping]`: 定義路由方法 +- `#[Inject]`: 依賴注入 +- `#[RemoteState]`: 遠程狀態管理 + +### 3. 服務層開發 + +基於 app-store 的 Service 實現模式: + +```php +service->getAppList($params); + } + + /** + * 下載應用 + */ + public function download(array $params): void + { + $app = $this->service->getAppInfo($params['identifier']); + + if (empty($app['download_url'])) { + throw new MineException('該應用無法下載', 500); + } + + if (Plugin::hasLocalInstalled($params['identifier'])) { + throw new MineException('應用已經存在本地,如需重新下載,請先刪除本地應用', 500); + } + + $this->service->download($params); + } + + /** + * 安裝應用 + */ + public function install(array $params): void + { + $pluginName = $params['name']; + + if (!Plugin::hasLocal($pluginName)) { + throw new MineException('插件不存在', 500); + } + + if (Plugin::hasLocalInstalled($pluginName)) { + throw new MineException('插件已經安裝', 500); + } + + Plugin::forceRefreshJsonPath($pluginName); + Plugin::install($pluginName); + } + + /** + * 卸載應用 + */ + public function unInstall(array $params): void + { + $pluginName = $params['name']; + + if (!Plugin::hasLocalInstalled($pluginName)) { + throw new MineException('插件未安裝', 500); + } + + Plugin::uninstall($pluginName); + } + + /** + * 獲取本地已安裝插件列表 + */ + public function getLocalAppInstallList(): array + { + $list = []; + $plugins = Plugin::getLocalPlugins(); + + foreach ($plugins as $name => $info) { + $app = ['identifier' => $name]; + $app['name'] = $info['name'] ?? '未知'; + $app['status'] = $info['status'] ?? false; + $app['version'] = $info['version'] ?? '0.0.0'; + $app['description'] = $info['description'] ?? '暫無描述'; + $app['created_at'] = $info['created_at'] ?? ''; + $list[] = $app; + } + + return $list; + } + + /** + * 本地上傳安裝 + */ + public function uploadLocalApp(array $params): void + { + if (empty($params['path'])) { + throw new MineException('請上傳插件包', 500); + } + + // 解壓並驗證插件包 + $zipFile = new \ZipArchive(); + $result = $zipFile->open($params['path']); + + if ($result !== true) { + throw new MineException('插件包解壓失敗', 500); + } + + // 獲取插件信息並安裝 + $mineJson = $zipFile->getFromName('mine.json'); + if (!$mineJson) { + throw new MineException('插件包格式錯誤,缺少mine.json', 500); + } + + $config = json_decode($mineJson, true); + $pluginName = $config['name'] ?? null; + + if (!$pluginName) { + throw new MineException('插件包配置錯誤', 500); + } + + // 解壓到插件目錄 + $targetPath = Plugin::getPluginPath($pluginName); + $zipFile->extractTo($targetPath); + $zipFile->close(); + + // 刷新緩存並安裝 + Plugin::forceRefreshJsonPath($pluginName); + Plugin::install($pluginName); + } +} +``` + +### 4. 模型層(如需數據庫) + +參考 code-generator 插件的模型實現: + +```php + 'boolean', + 'is_list' => 'boolean', + 'is_query' => 'boolean', + 'is_required' => 'boolean', + 'is_sort' => 'boolean', + 'is_edit' => 'boolean', + 'is_readonly' => 'boolean', + ]; +} +``` + +## 前端開發 + +### 1. 插件入口文件 (index.ts) + +基於 app-store 的前端實現: + +```typescript +import type { App } from 'vue' +import type { Plugin } from '#/global' + +const pluginConfig: Plugin.PluginConfig = { + install(app: App) { + // Vue插件安裝鈎子 + console.log('app-store plugin install') + }, + config: { + enable: true, + info: { + name: 'app-store', + version: '1.0.0', + author: 'MineAdmin Team', + description: 'MineAdmin應用市場可視化插件' + } + }, + views: [ + { + name: 'plugin:store', + path: '/plugin/store', + meta: { + title: 'app_store.app_store', + i18n: true, + icon: 'material-symbols:app-shortcut', + type: 'M', + hidden: false, + componentPath: '/plugin/mine-admin/app-store/views/index.vue', + componentName: 'plugin:mine-admin:app-store:index', + }, + component: () => import('./views/index.vue'), + } + ], +} + +export default pluginConfig +``` + +### 2. API 接口封裝 + +```typescript +// api/app-store.ts +import { request } from '@/utils/request' + +// 獲取遠程插件列表 +export const getAppList = (params: any) => { + return request.get('/admin/plugin/store/index', { params }) +} + +// 下載插件 +export const downloadApp = (data: any) => { + return request.post('/admin/plugin/store/download', data) +} + +// 安裝插件 +export const installApp = (data: any) => { + return request.post('/admin/plugin/store/install', data) +} + +// 卸載插件 +export const uninstallApp = (data: any) => { + return request.post('/admin/plugin/store/unInstall', data) +} + +// 獲取本地已安裝插件 +export const getInstalledList = () => { + return request.get('/admin/plugin/store/getInstallList') +} + +// 上傳本地插件安裝 +export const uploadInstall = (data: any) => { + return request.post('/admin/plugin/store/uploadInstall', data) +} +``` + +### 3. Vue 組件開發 + +```vue + + + + +``` + +### 4. 國際化支持 + +```typescript +// locales/zh_CN.ts +export default { + app_store: { + app_store: '應用市場', + app_list: '應用列表', + installed: '已安裝', + install: '安裝', + uninstall: '卸載', + download: '下載', + upload: '上傳', + local_upload: '本地上傳', + upload_tips: '請選擇插件包文件(.zip格式)', + } +} +``` + +## 安裝與卸載腳本 + +### InstallScript.php + +基於 code-generator 插件的實際實現: + +```php +output = new ConsoleOutput(); + + try { + $this->info('========================================'); + $this->info('MineAdmin 代碼生成器插件'); + $this->info('========================================'); + $this->info('開始安裝插件...'); + + // 1. 複製模板文件 + $this->copyTemplates(); + + // 2. 複製語言包 + $this->copyLanguages(); + + // 3. 發佈依賴資源 + $this->publishVendor(); + + // 4. 執行數據庫遷移 + $this->runMigrations(); + + $this->info('插件安裝成功!'); + $this->info('========================================'); + + } catch (\Throwable $e) { + $this->error('插件安裝失敗:' . $e->getMessage()); + throw $e; + } + } + + /** + * 複製模板文件 + */ + protected function copyTemplates(): void + { + $source = dirname(__DIR__) . '/publish/template'; + $target = BASE_PATH . '/runtime/generate/template'; + + if (!is_dir($target)) { + mkdir($target, 0755, true); + } + + Filesystem::copy($source, $target, false); + $this->info('模板文件複製成功'); + } + + /** + * 複製語言包 + */ + protected function copyLanguages(): void + { + $source = dirname(__DIR__) . '/languages'; + $target = BASE_PATH . '/storage/languages'; + + Filesystem::copy($source, $target, false); + $this->info('語言包複製成功'); + } + + /** + * 發佈依賴包資源 + */ + protected function publishVendor(): void + { + $app = ApplicationContext::getContainer()->get(ApplicationInterface::class); + $app->setAutoExit(false); + + $input = new ArrayInput([ + 'command' => 'vendor:publish', + 'package' => 'hyperf/translation', + ]); + + $app->run($input, new NullOutput()); + $this->info('依賴資源發佈成功'); + } + + /** + * 執行數據庫遷移 + */ + protected function runMigrations(): void + { + $migrationPath = dirname(__DIR__) . '/Database/Migrations'; + + if (!is_dir($migrationPath)) { + return; + } + + $app = ApplicationContext::getContainer()->get(ApplicationInterface::class); + $app->setAutoExit(false); + + $input = new ArrayInput([ + 'command' => 'migrate', + '--path' => $migrationPath, + '--force' => true, + ]); + + $app->run($input, new NullOutput()); + $this->info('數據庫遷移執行成功'); + } +} +``` + +### UninstallScript.php + +```php +output = new ConsoleOutput(); + + $this->info('========================================'); + $this->info('即將卸載代碼生成器插件'); + $this->info('========================================'); + + try { + // 清理模板文件 + $this->cleanTemplates(); + + // 清理語言包 + $this->cleanLanguages(); + + // 清理數據庫(可選,根據需求決定是否清理) + if ($this->confirm('是否清理數據庫表?')) { + $this->cleanDatabase(); + } + + $this->info('插件卸載成功!'); + + } catch (\Throwable $e) { + $this->error('插件卸載失敗:' . $e->getMessage()); + throw $e; + } + } + + protected function cleanTemplates(): void + { + $templatePath = BASE_PATH . '/runtime/generate/template'; + if (is_dir($templatePath)) { + // 遞歸刪除目錄 + $this->removeDirectory($templatePath); + $this->info('模板文件清理成功'); + } + } + + protected function cleanLanguages(): void + { + // 清理語言包文件 + $langFile = BASE_PATH . '/storage/languages/zh_CN/code-generator.php'; + if (file_exists($langFile)) { + unlink($langFile); + $this->info('語言包清理成功'); + } + } + + protected function cleanDatabase(): void + { + // 執行數據庫清理 + // 注意:這裏需要謹慎處理,避免誤刪用户數據 + $this->info('數據庫表清理成功'); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + is_dir($path) ? $this->removeDirectory($path) : unlink($path); + } + rmdir($dir); + } +} +``` + +## 數據庫遷移 + +基於 code-generator 的遷移文件: + +```php +engine = 'InnoDB'; + $table->comment('生成業務表'); + $table->bigIncrements('id')->comment('主鍵'); + $table->string('table_name', 200)->comment('表名稱'); + $table->string('table_comment', 500)->comment('表註釋'); + $table->string('module_name', 100)->comment('模塊名'); + $table->string('namespace', 255)->comment('命名空間'); + $table->string('menu_name', 100)->comment('菜單名稱'); + $table->bigInteger('belong_menu_id')->nullable()->comment('所屬菜單'); + $table->string('package_name', 100)->nullable()->comment('包名'); + $table->addColumn('string', 'type', ['length' => 100])->comment('生成類型'); + $table->addColumn('string', 'generate_mode', ['length' => 30])->default('1')->comment('生成方式'); + $table->addColumn('string', 'generate_menus', ['length' => 255])->nullable()->comment('生成菜單列表'); + $table->addColumn('string', 'build_menu', ['length' => 10])->default('1')->comment('構建菜單'); + $table->addColumn('string', 'component_type', ['length' => 30])->default('1')->comment('組件類型'); + $table->json('options')->nullable()->comment('其他配置'); + $table->bigInteger('created_by')->comment('創建者'); + $table->bigInteger('updated_by')->comment('更新者'); + $table->datetimes(); + $table->unique('table_name'); + $table->index('table_name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('setting_generate_tables'); + } +}; +``` + +## 測試與調試 + +### 1. 本地安裝測試 + +```bash +# 創建插件 +php bin/hyperf.php mine-extension:create mine-admin/my-plugin + +# 安裝插件 +php bin/hyperf.php mine-extension:install mine-admin/my-plugin + +# 查看已安裝插件 +php bin/hyperf.php mine-extension:local-list + +# 卸載插件 +php bin/hyperf.php mine-extension:uninstall mine-admin/my-plugin +``` + +### 2. 調試技巧 + +```php +// 在服務層添加日誌 +use Hyperf\Context\ApplicationContext; +use Psr\Log\LoggerInterface; + +$logger = ApplicationContext::getContainer()->get(LoggerInterface::class); +$logger->info('調試信息', ['data' => $data]); + +// 使用 dd() 函數調試 +dd($variable); + +// 使用異常拋出調試 +throw new \Exception('調試信息: ' . json_encode($data)); +``` + +### 3. 前端調試 + +```typescript +// 在瀏覽器控制枱查看 +console.log('調試信息', data) + +// 使用 Vue DevTools 調試組件狀態 + +// 查看網絡請求 +// 使用瀏覽器的 Network 面板查看 API 請求和響應 +``` + +## 開發最佳實踐 ⭐ + +### 1. 代碼規範 + +- **命名規範**: + - 插件名:`vendor/plugin-name` 格式 + - 命名空間:`Plugin\Vendor\PluginName` + - 類名:PascalCase + - 方法名:camelCase + +- **PSR規範**: + - 遵循 PSR-4 自動加載規範 + - 遵循 PSR-12 編碼規範 + +### 2. 目錄組織原則 + +- 後端代碼統一放在 `src/` 目錄 +- 前端代碼統一放在 `web/` 目錄 +- 數據庫相關放在 `Database/` 目錄 +- 靜態資源放在 `publish/` 目錄 +- 語言包放在 `languages/` 和 `web/locales/` 目錄 + +### 3. 配置管理 (重要) + +- **不要依賴 ConfigProvider 的 publish 功能** +- **在 InstallScript 中處理所有文件複製和配置發佈** +- **在 InstallScript 中執行數據庫遷移** +- **在 InstallScript 中進行環境檢測** + +### 4. 安全考慮 + +```php +// 參數驗證 +use Hyperf\Validation\Request\FormRequest; + +class StoreRequest extends FormRequest +{ + public function rules(): array + { + return [ + 'name' => 'required|string|max:100', + 'email' => 'required|email', + ]; + } +} + +// 權限控制 +#[Permission("plugin:module:action")] +public function action() {} + +// SQL注入防護 - 使用參數綁定 +$model->where('name', '=', $name)->get(); + +// XSS防護 - 前端處理 +{{ data | escape }} +``` + +### 5. 性能優化 + +```php +// 使用依賴注入減少耦合 +#[Inject] +protected Service $service; + +// 使用緩存 +use Hyperf\Cache\Annotation\Cacheable; + +#[Cacheable(prefix: "plugin", ttl: 3600)] +public function getData() {} + +// 路由懶加載 +component: () => import('./views/index.vue') + +// 數據庫查詢優化 +$query->select(['id', 'name'])->with('relation')->limit(20); +``` + +### 6. 錯誤處理 + +```php +use Mine\Exception\MineException; + +// 業務異常 +if (!$condition) { + throw new MineException('錯誤信息', 500); +} + +// try-catch 處理 +try { + // 業務邏輯 +} catch (\Throwable $e) { + $this->logger->error('操作失敗', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + throw new MineException('操作失敗: ' . $e->getMessage()); +} +``` + +## 常見問題解決 + +### Q: 插件安裝後無法訪問? +A: +1. 檢查 ConfigProvider 的 annotations 配置是否正確 +2. 確認控制器的 #[Controller] 註解路由前綴 +3. 檢查權限註解 #[Permission] 是否已在系統中配置 + +### Q: 配置文件沒有發佈? +A: ConfigProvider 的 publish 功能在插件中不可靠,請在 InstallScript 中手動處理配置發佈。 + +### Q: 數據庫遷移失敗? +A: +1. 檢查數據庫連接配置 +2. 確認遷移文件路徑正確 +3. 查看遷移命令的錯誤輸出 + +### Q: 前端組件不顯示? +A: +1. 檢查 web/index.ts 的路由配置 +2. 確認組件路徑正確 +3. 查看瀏覽器控制枱錯誤信息 + +### Q: 依賴包衝突? +A: +1. 在 mine.json 中正確配置 composer 依賴版本約束 +2. 使用 `composer update` 更新依賴 +3. 檢查與主項目的依賴兼容性 + +## 相關文檔 + +- [插件結構詳解](./structure.md) +- [生命週期管理](./lifecycle.md) +- [API 參考文檔](./api.md) +- [示例代碼](./examples.md) +- [mine.json 配置](./mineJson.md) +- [ConfigProvider 説明](./configProvider.md) \ No newline at end of file diff --git a/docs/zh-hk/plugin/examples.md b/docs/zh-hk/plugin/examples.md new file mode 100644 index 0000000..9552d19 --- /dev/null +++ b/docs/zh-hk/plugin/examples.md @@ -0,0 +1,1396 @@ +# 插件示例代碼 + +本文檔提供完整的 MineAdmin 插件開發示例,包括不同類型插件的實際代碼案例和最佳實踐。 + +## 官方插件示例 + +### App-Store 插件 (混合型) + +**倉庫地址**: [mineadmin/appstore](https://github.com/mineadmin/appstore) + +App-Store 是 MineAdmin 唯一的官方默認插件,提供應用市場管理功能,展示了混合型插件的完整實現。 + +#### 核心文件結構 +``` +plugin/mine-admin/app-store/ +├── mine.json # 插件配置 +├── src/ # 後端代碼 +│ ├── ConfigProvider.php # 配置提供者 +│ ├── Controller/ # 控制器 +│ ├── Service/ # 服務層 +│ └── Command/ # 命令行 +├── web/ # 前端代碼 +│ ├── views/ # 頁面組件 +│ └── api/ # API 接口 +└── Database/ # 數據庫 +``` + +#### mine.json 配置示例 +```json +{ + "name": "mine-admin/app-store", + "description": "MineAdmin應用市場可視化插件", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "MineAdmin Team", + "role": "developer" + } + ], + "keywords": ["mineadmin", "app-store", "plugin-management"], + "homepage": "https://github.com/mineadmin/appstore", + "license": "MIT", + "composer": { + "require": { + "hyperf/async-queue": "^3.0" + }, + "psr-4": { + "Plugin\\MineAdmin\\AppStore\\": "src" + }, + "config": "Plugin\\MineAdmin\\AppStore\\ConfigProvider" + } +} +``` + +#### ConfigProvider 實現 +```php + [ + // 依賴注入配置 + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'commands' => [ + Command\AppStoreCommand::class, + ], + 'listeners' => [ + Listener\PluginEventListener::class, + ], + 'publish' => [ + [ + 'id' => 'appstore-config', + 'description' => 'App Store configuration file', + 'source' => __DIR__ . '/../publish/appstore.php', + 'destination' => BASE_PATH . '/config/autoload/appstore.php', + ], + ], + ]; + } +} +``` + +## 完整插件開發示例 + +### 1. 用户管理插件 (混合型) + +以下是一個完整的用户管理插件示例,展示如何開發一個包含前後端的混合型插件。 + +#### mine.json 配置 +```json +{ + "name": "mycompany/user-manager", + "description": "用户管理插件", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "composer": { + "require": { + "hyperf/database": "^3.0", + "hyperf/validation": "^3.0" + }, + "psr-4": { + "Plugin\\MyCompany\\UserManager\\": "src" + }, + "config": "Plugin\\MyCompany\\UserManager\\ConfigProvider" + }, + "package": { + "dependencies": { + "element-plus": "^2.4.0" + } + } +} +``` + +#### 核心控制器實現 +```php +request->all(); + $result = $this->service->getPageList($params); + return $this->success($result); + } + + #[PostMapping('/user')] + public function create(): array + { + $data = $this->request->all(); + $user = $this->service->create($data); + return $this->success($user, '用户創建成功'); + } + + #[PutMapping('/user/{id}')] + public function update(int $id): array + { + $data = $this->request->all(); + $this->service->update($id, $data); + return $this->success(null, '更新成功'); + } + + #[DeleteMapping('/user/{id}')] + public function delete(int $id): array + { + $this->service->delete($id); + return $this->success(null, '刪除成功'); + } +} +``` + +#### 服務層實現 +```php +where(function($q) use ($params) { + $q->where('username', 'like', "%{$params['keyword']}%") + ->orWhere('email', 'like', "%{$params['keyword']}%"); + }); + } + + $paginator = $query->paginate( + $params['pageSize'] ?? 15, + ['*'], + 'page', + $params['page'] ?? 1 + ); + + return [ + 'items' => $paginator->items(), + 'pageInfo' => [ + 'total' => $paginator->total(), + 'currentPage' => $paginator->currentPage(), + 'totalPage' => $paginator->lastPage() + ] + ]; + } + + public function create(array $data): User + { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + return User::create($data); + } + + public function update(int $id, array $data): bool + { + if (isset($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } + + return User::query()->where('id', $id)->update($data) > 0; + } + + public function delete(int $id): bool + { + return User::destroy($id) > 0; + } +} +``` + +### 2. 後端型插件示例 - API 服務插件 + +以下是一個純後端 API 服務插件的示例。 + +#### 插件配置 (mine.json) + +```json +{ + "name": "mycompany/api-service", + "description": "API 服務插件", + "version": "1.0.0", + "type": "backend", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "keywords": ["api", "service"], + "license": "MIT", + "composer": { + "require": { + "guzzlehttp/guzzle": "^7.0" + }, + "psr-4": { + "Plugin\\MyCompany\\ApiService\\": "src" + }, + "config": "Plugin\\MyCompany\\ApiService\\ConfigProvider" + } +} +``` + +#### ConfigProvider 實現 + +```php + [ + Contract\ApiClientInterface::class => Service\ApiClient::class, + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'commands' => [ + Command\ApiSyncCommand::class, + ], + 'publish' => [ + [ + 'id' => 'api-service-config', + 'description' => 'API 服務配置文件', + 'source' => __DIR__ . '/../publish/api_service.php', + 'destination' => BASE_PATH . '/config/autoload/api_service.php', + ], + ], + ]; + } +} +``` + +### 3. 前端型插件示例 - 數據可視化插件 + +以下是一個純前端的數據可視化插件示例。 + +#### mine.json 配置 +```json +{ + "name": "mycompany/data-visualization", + "description": "數據可視化插件", + "version": "1.0.0", + "type": "frontend", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "package": { + "dependencies": { + "echarts": "^5.4.0", + "vue-echarts": "^6.5.0" + } + } +} +``` + +## 完整插件開發最佳實踐 + +### 1. 目錄結構規範 + +``` +plugin/vendor-name/plugin-name/ +├── mine.json # 插件配置文件 +├── src/ # PHP 後端代碼 +│ ├── ConfigProvider.php # 配置提供者 +│ ├── Controller/ # 控制器 +│ ├── Service/ # 服務層 +│ ├── Model/ # 模型 +│ ├── Command/ # 命令行 +│ ├── Listener/ # 事件監聽器 +│ └── Middleware/ # 中間件 +├── web/ # 前端代碼 +│ ├── views/ # Vue 頁面組件 +│ ├── api/ # API 接口封裝 +│ ├── components/ # 公共組件 +│ └── locales/ # 國際化 +├── Database/ # 數據庫 +│ ├── Migrations/ # 遷移文件 +│ └── Seeders/ # 數據填充 +└── publish/ # 發佈文件 + └── config.php # 配置文件 +``` + +### 2. 命名規範 + +- **插件名稱**: 使用 `vendor/plugin-name` 格式 +- **命名空間**: `Plugin\VendorName\PluginName` +- **類名**: 使用大駝峯命名法 +- **方法名**: 使用小駝峯命名法 + +### 3. 核心組件示例 + +#### 控制器示例 + +```php +request->all(); + $result = $this->service->getList($params); + return $this->success($result); + } + + #[PostMapping('/create')] + public function create(): array + { + $data = $this->request->all(); + $result = $this->service->create($data); + return $this->success($result, '創建成功'); + } + $user = $this->userService->find($id); + + if (!$user) { + return $this->error('用户不存在', 404); + } + + return $this->success($user); + } + + /** + * 更新用户 + */ + #[PutMapping('/users/{id:\d+}')] + public function update(int $id): array + { + $data = $this->request->all(); + + $user = $this->userService->update($id, $data); + + return $this->success($user, '用户更新成功'); + } + + /** + * 刪除用户 + */ + #[DeleteMapping('/users/{id:\d+}')] + public function destroy(int $id): array + { + $this->userService->delete($id); + + return $this->success([], '用户刪除成功'); + } + + /** + * 批量導入用户 + */ + #[PostMapping('/users/import')] + public function import(): array + { + $file = $this->request->file('file'); + + if (!$file || !$file->isValid()) { + return $this->error('請上傳有效的文件'); + } + + $result = $this->userService->importFromFile($file); + + return $this->success($result, '導入完成'); + } + + /** + * 導出用户數據 + */ + #[GetMapping('/users/export')] + public function export(): array + { + $params = $this->request->all(); + $filePath = $this->userService->exportToFile($params); + + return $this->success(['file_path' => $filePath], '導出成功'); + } +} +``` + +#### 4. 服務層 (src/Service/UserService.php) + +```php +repository->getList($params); + } + + /** + * 創建用户 + */ + public function create(array $data): array + { + // 密碼加密 + if (isset($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } + + // 生成用户頭像 + if (!isset($data['avatar'])) { + $data['avatar'] = $this->generateAvatar($data['username']); + } + + $user = $this->repository->create($data); + + // 觸發用户創建事件 + event(new UserCreatedEvent($user)); + + return $user->toArray(); + } + + /** + * 更新用户 + */ + public function update(int $id, array $data): array + { + // 密碼更新處理 + if (isset($data['password']) && !empty($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } else { + unset($data['password']); + } + + $user = $this->repository->update($id, $data); + + // 觸發用户更新事件 + event(new UserUpdatedEvent($user)); + + return $user->toArray(); + } + + /** + * 從文件導入用户 + */ + public function importFromFile($file): array + { + $filePath = $file->getPath() . '/' . $file->getFilename(); + + // 讀取 Excel 文件 + $data = $this->parseExcelFile($filePath); + + $successCount = 0; + $errorCount = 0; + $errors = []; + + foreach ($data as $index => $row) { + try { + $this->create([ + 'username' => $row['username'], + 'email' => $row['email'], + 'phone' => $row['phone'] ?? null, + 'password' => $row['password'] ?? '123456', + ]); + $successCount++; + } catch (\Exception $e) { + $errorCount++; + $errors[] = "第{$index}行: " . $e->getMessage(); + } + } + + return [ + 'success_count' => $successCount, + 'error_count' => $errorCount, + 'errors' => $errors + ]; + } + + /** + * 導出用户到文件 + */ + public function exportToFile(array $params = []): string + { + $users = $this->repository->getAllForExport($params); + + // 生成 Excel 文件 + $filePath = $this->generateExcelFile($users); + + return $filePath; + } + + /** + * 生成用户頭像 + */ + private function generateAvatar(string $username): string + { + // 使用第三方庫生成頭像 + $avatar = new \Intervention\Image\ImageManager(); + // ... 頭像生成邏輯 + + return '/uploads/avatars/' . $username . '.png'; + } + + /** + * 解析 Excel 文件 + */ + private function parseExcelFile(string $filePath): array + { + // Excel 解析邏輯 + return []; + } + + /** + * 生成 Excel 文件 + */ + private function generateExcelFile(array $users): string + { + // Excel 生成邏輯 + return '/tmp/users_export_' . date('YmdHis') . '.xlsx'; + } + + protected function getRepository(): string + { + return UserRepository::class; + } +} +``` + +#### 5. 數據倉庫 (src/Repository/UserRepository.php) + +```php +getModel()::query(); + + // 關鍵詞搜索 + if (!empty($params['keyword'])) { + $query->where(function ($q) use ($params) { + $q->where('username', 'like', "%{$params['keyword']}%") + ->orWhere('email', 'like', "%{$params['keyword']}%") + ->orWhere('phone', 'like', "%{$params['keyword']}%"); + }); + } + + // 狀態篩選 + if (isset($params['status'])) { + $query->where('status', $params['status']); + } + + // 角色篩選 + if (!empty($params['role_id'])) { + $query->whereHas('roles', function ($q) use ($params) { + $q->where('id', $params['role_id']); + }); + } + + // 時間範圍篩選 + if (!empty($params['created_at'])) { + $dates = explode(' - ', $params['created_at']); + if (count($dates) === 2) { + $query->whereBetween('created_at', [ + $dates[0] . ' 00:00:00', + $dates[1] . ' 23:59:59' + ]); + } + } + + // 排序 + $query->orderBy($params['sort'] ?? 'id', $params['order'] ?? 'desc'); + + return $query->paginate($params['pageSize'] ?? 15)->toArray(); + } + + /** + * 獲取導出數據 + */ + public function getAllForExport(array $params = []): array + { + $query = $this->getModel()::query(); + + // 應用相同的篩選條件 + // ... 篩選邏輯 + + return $query->select([ + 'id', 'username', 'email', 'phone', + 'status', 'created_at', 'updated_at' + ])->get()->toArray(); + } +} +``` + +#### 6. 模型 (src/Model/User.php) + +```php + 'integer', + 'last_login_at' => 'datetime:Y-m-d H:i:s', + 'created_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', + ]; + + /** + * 狀態常量 + */ + const STATUS_DISABLED = 0; + const STATUS_ENABLED = 1; + + /** + * 關聯角色 + */ + public function roles() + { + return $this->belongsToMany(Role::class, 'user_roles'); + } + + /** + * 獲取狀態文本 + */ + public function getStatusTextAttribute(): string + { + return match($this->status) { + self::STATUS_ENABLED => '啓用', + self::STATUS_DISABLED => '禁用', + default => '未知' + }; + } + + /** + * 獲取頭像 URL + */ + public function getAvatarUrlAttribute(): string + { + if (empty($this->avatar)) { + return '/default-avatar.png'; + } + + return str_starts_with($this->avatar, 'http') + ? $this->avatar + : config('app.url') . $this->avatar; + } +} +``` + +#### 7. 前端頁面 (web/views/UserList.vue) + +```vue + + + + + +``` + +#### 8. API 接口 (web/api/user.js) + +```javascript +// web/api/user.js +import { request } from '@/utils/request' + +const API_BASE = '/user-manager' + +export default { + // 獲取用户列表 + getList(params) { + return request({ + url: `${API_BASE}/users`, + method: 'get', + params + }) + }, + + // 創建用户 + create(data) { + return request({ + url: `${API_BASE}/users`, + method: 'post', + data + }) + }, + + // 獲取用户詳情 + get(id) { + return request({ + url: `${API_BASE}/users/${id}`, + method: 'get' + }) + }, + + // 更新用户 + update(id, data) { + return request({ + url: `${API_BASE}/users/${id}`, + method: 'put', + data + }) + }, + + // 刪除用户 + delete(id) { + return request({ + url: `${API_BASE}/users/${id}`, + method: 'delete' + }) + }, + + // 批量導入 + import(file) { + const formData = new FormData() + formData.append('file', file) + + return request({ + url: `${API_BASE}/users/import`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + // 導出數據 + export(params) { + return request({ + url: `${API_BASE}/users/export`, + method: 'get', + params + }) + } +} +``` + +#### 9. 數據庫遷移 (Database/Migrations/create_users_table.php) + +```php +id(); + $table->string('username', 50)->unique()->comment('用户名'); + $table->string('email')->unique()->comment('郵箱'); + $table->string('phone', 20)->nullable()->comment('手機號'); + $table->string('password')->comment('密碼'); + $table->string('avatar')->nullable()->comment('頭像'); + $table->tinyInteger('status')->default(1)->comment('狀態:0禁用,1啓用'); + $table->timestamp('last_login_at')->nullable()->comment('最後登錄時間'); + $table->timestamps(); + + $table->index(['username']); + $table->index(['email']); + $table->index(['phone']); + $table->index(['status']); + $table->index(['created_at']); + + $table->comment('用户管理插件-用户表'); + }); + } + + public function down(): void + { + Schema::dropIfExists('plugin_user_manager_users'); + } +} +``` + +#### 10. 安裝腳本 (src/InstallScript.php) + +```php +runMigrations(); + + // 2. 初始化數據 + $this->seedData(); + + // 3. 創建必要目錄 + $this->createDirectories(); + + // 4. 初始化配置 + $this->initConfig(); + + echo "用户管理插件安裝成功!\n"; + return true; + } catch (\Exception $e) { + echo "安裝失敗: " . $e->getMessage() . "\n"; + return false; + } + } + + private function runMigrations(): void + { + $migrationPath = __DIR__ . '/../Database/Migrations'; + + if (!is_dir($migrationPath)) { + return; + } + + $files = glob($migrationPath . '/*.php'); + sort($files); + + foreach ($files as $file) { + require_once $file; + + $className = $this->getMigrationClassName($file); + $migration = new $className(); + + if (method_exists($migration, 'up')) { + $migration->up(); + echo "執行遷移: " . basename($file) . "\n"; + } + } + } + + private function seedData(): void + { + // 插入默認管理員用户 + Db::table('plugin_user_manager_users')->insertOrIgnore([ + 'username' => 'admin', + 'email' => 'admin@example.com', + 'password' => password_hash('123456', PASSWORD_DEFAULT), + 'status' => 1, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + echo "初始化默認數據完成\n"; + } + + private function createDirectories(): void + { + $directories = [ + BASE_PATH . '/public/uploads/avatars', + BASE_PATH . '/storage/user-manager', + ]; + + foreach ($directories as $dir) { + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + echo "創建目錄: {$dir}\n"; + } + } + } + + private function initConfig(): void + { + $configPath = BASE_PATH . '/config/autoload/user_manager.php'; + + if (!file_exists($configPath)) { + $defaultConfig = [ + 'avatar_upload_path' => '/uploads/avatars', + 'default_password' => '123456', + 'password_reset_expire' => 3600, + 'max_login_attempts' => 5, + ]; + + file_put_contents($configPath, " [ + // 依賴注入配置 + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'publish' => [ + // 配置文件發佈設置 + ], + ]; + } +} +``` + +### 3. 添加業務邏輯 + +創建控制器 `src/Controller/HelloController.php`: + +```php + 200, + 'message' => 'Hello from MineAdmin Plugin!', + 'data' => [ + 'plugin' => 'hello-world', + 'timestamp' => time() + ] + ]; + } +} +``` + +### 4. 前端開發 (可選) + +在 `web/` 目錄下添加前端組件: + +```vue + + + + +``` + +## 安裝和測試插件 + +### 1. 安裝插件 + +```bash +# 安裝插件到系統 +php bin/hyperf.php mine-extension:install mycompany/hello-world --yes +``` + +### 2. 測試功能 + +啓動開發服務器並測試 API: + +```bash +# 啓動服務 +php bin/hyperf.php start + +# 測試 API (新終端) +curl http://localhost:9501/hello-world/greeting +``` + +### 3. 檢查安裝狀態 + +```bash +# 查看本地已安裝插件 +php bin/hyperf.php mine-extension:local-list +``` + +## 插件管理命令 + +### 常用命令總覽 + +```bash +# 查看遠程插件列表 +php bin/hyperf.php mine-extension:list + +# 下載遠程插件 +php bin/hyperf.php mine-extension:download --name plugin-name + +# 安裝本地插件 +php bin/hyperf.php mine-extension:install plugin/path --yes + +# 卸載插件 +php bin/hyperf.php mine-extension:uninstall plugin/path --yes + +# 查看本地插件 +php bin/hyperf.php mine-extension:local-list +``` + +## 開發調試技巧 + +### 1. 日誌調試 + +在插件中使用 Hyperf 日誌系統: + +```php +use Hyperf\Logger\LoggerFactory; + +$logger = $container->get(LoggerFactory::class)->get('plugin'); +$logger->info('Hello World Plugin Debug', ['data' => $someData]); +``` + +### 2. 配置熱重載 + +開發期間修改配置後需要重啓服務: + +```bash +# 重啓 Hyperf 服務 +php bin/hyperf.php start +``` + +### 3. 前端熱更新 + +如果使用 MineAdmin 前端開發環境: + +```bash +# 在前端項目目錄 +npm run dev +``` + +## 下一步 + +現在您已經創建了第一個插件!接下來可以: + +1. [深入瞭解插件結構](./structure.md) +2. [學習完整開發流程](./develop.md) +3. [瞭解生命週期管理](./lifecycle.md) +4. [查看更多示例](./examples.md) + +## 常見問題 + +### Q: 插件安裝失敗怎麼辦? +A: 檢查 `mine.json` 配置是否正確,確保 PSR-4 自動加載路徑正確。 + +### Q: 如何調試插件? +A: 使用 Hyperf 的日誌系統和調試工具,查看 `runtime/logs/` 目錄下的日誌文件。 + +### Q: 前端組件不顯示? +A: 確保前端文件放在 `web/` 目錄下,安裝插件時會自動複製到前端項目。 \ No newline at end of file diff --git a/docs/zh-hk/plugin/index.md b/docs/zh-hk/plugin/index.md index d16c879..9c63706 100644 --- a/docs/zh-hk/plugin/index.md +++ b/docs/zh-hk/plugin/index.md @@ -1,49 +1,117 @@ -# 準備工作 +# MineAdmin 插件系統 -::: tip -如果開發 MineAdmin 應用;首先,得熟悉 MineAdmin 和 Hyperf 框架,然後做以下的準備工作。 -::: +MineAdmin 插件系統提供了強大的擴展能力,允許開發者創建可複用的功能模塊,實現系統的模塊化和可擴展性。 -## 獲取AccessToken +## 插件系統架構 -MineAdmin不管下載插件應用、更新插件應用還是開發插件應用都需要 `ACCESS_TOKEN` +MineAdmin 的插件系統基於 Hyperf 框架的 ConfigProvider 機制,提供了完整的插件生命週期管理和自動化部署能力。 -獲取步驟: +```plantuml +@startuml +!define RECTANGLE class -- 登錄 [MineAdmin](https://www.mineadmin.com/login) 官網 -- 進入 `個人中心` 的 [_設置_](https://www.mineadmin.com/member/setting) 頁面 -- 點擊查看`我的AccessToken` +RECTANGLE "MineAdmin Core" as core { + + bin/hyperf.php + + Plugin::init() +} -::: danger +RECTANGLE "Plugin Management" as mgmt { + + App-Store Component + + Extension Commands + + Plugin Loader +} ---- - -注意 +RECTANGLE "Plugin Structure" as structure { + + mine.json (配置文件) + + src/ (後端代碼) + + web/ (前端代碼) + + Database/ (數據庫) +} -請保管好自己的 AccessToken,不要泄露!!! +RECTANGLE "Official Plugins" as official { + + app-store +} ---- +core --> mgmt : 插件初始化 +mgmt --> structure : 加載插件 +mgmt --> official : 管理官方插件 +structure --> core : 註冊服務 -::: +@enduml +``` + +## 核心組件 + +### 1. 插件加載器 +- **文件**: `bin/hyperf.php` ([GitHub](https://github.com/mineadmin/mineadmin/blob/master/bin/hyperf.php)) +- **原理**: 通過 `Plugin::init()` 方法在應用啓動時自動加載所有已安裝的插件 +- **實現**: 掃描 `plugin/` 目錄下的所有插件並註冊其 ConfigProvider + +### 2. App-Store 組件 +- **倉庫**: [mineadmin/appstore](https://github.com/mineadmin/appstore) +- **功能**: 提供插件的下載、安裝、卸載、更新等管理功能 +- **配置**: 通過 `ConfigProvider` 註冊服務和配置 + +### 3. 插件配置系統 +- **核心文件**: `mine.json` +- **原理**: 定義插件的元數據、依賴關係、安裝腳本等信息 +- **加載**: 在插件安裝時解析並註冊到系統中 + +## 官方插件 + +MineAdmin 默認提供以下官方插件: -## 配置後端 .env 文件 +| 插件名稱 | 功能描述 | 倉庫地址 | +|---------|----------|----------| +| app-store | 應用市場管理插件,提供插件的下載、安裝、卸載、更新等管理功能 | [GitHub](https://github.com/mineadmin/appstore) | -打開後端根目錄的 _.env_ 文件,找到 **MINE_ACCESS_TOKEN** 項,把剛複製的字符串粘貼到 **等號** 後面 +> 注:其他插件如代碼生成器、定時任務管理等可通過應用市場獲取或自行開發 -```ini [.env] -APP_NAME = MineAdmin +## 插件類型 -APP_ENV = dev +MineAdmin 支持三種類型的插件: -# 省略... +### Mixed (混合型插件) +包含前端和後端完整功能的插件,提供完整的業務模塊。 -MINE_ACCESS_TOKEN = 107299501236086 +### Backend (後端插件) +僅包含後端邏輯的插件,主要提供 API 服務和業務邏輯。 + +### Frontend (前端插件) +僅包含前端界面的插件,主要提供用户界面組件。 + +## 快速開始 + +### 環境準備 + +開發 MineAdmin 插件需要: + +1. **熟悉技術棧**:MineAdmin 和 Hyperf 框架 +2. **獲取 AccessToken**: + - 登錄 [MineAdmin 官網](https://www.mineadmin.com/login) + - 進入個人中心 → [設置頁面](https://www.mineadmin.com/member/setting) + - 獲取 AccessToken + +3. **配置環境變量**: +```ini +# .env 文件 +MINE_ACCESS_TOKEN=你的AccessToken ``` -## 申請開發者 +::: warning 注意 +請妥善保管 AccessToken,避免泄露! +::: + +### 開發者認證 -如果僅是本地開發應用並且自己使用,這種情況不需要有開發者認證權限,你也可以分發給其他任何人。 +- **本地開發**:無需認證,可自由開發和分發 +- **應用市場發佈**:需要開發者認證,聯繫 MineAdmin 團隊開通權限 -如果打算上架官方應用市場,需要進行開發者認證後,才可發佈你的應用,並且受到官方版權保護。 +## 相關文檔 -目前還未支持線上直接申請認證,需要通過聯繫 **MineAdmin團隊成員** 給你開通開發者權限 \ No newline at end of file +- [快速入門指南](./guide.md) - 創建第一個插件 +- [開發指南](./develop.md) - 詳細開發流程 +- [插件結構](./structure.md) - 目錄結構規範 +- [生命週期管理](./lifecycle.md) - 安裝卸載流程 +- [API 參考](./api.md) - 接口文檔 +- [示例代碼](./examples.md) - 實際案例 \ No newline at end of file diff --git a/docs/zh-hk/plugin/lifecycle.md b/docs/zh-hk/plugin/lifecycle.md new file mode 100644 index 0000000..230d2cb --- /dev/null +++ b/docs/zh-hk/plugin/lifecycle.md @@ -0,0 +1,743 @@ +# 插件生命週期管理 + +詳細介紹 MineAdmin 插件的生命週期管理,包括安裝、啓用、禁用、更新和卸載的完整流程。 + +## 生命週期概覽 + +MineAdmin 插件的生命週期包括以下幾個階段: + +```plantuml +@startuml +!define RECTANGLE class + +state "未安裝" as uninstalled +state "已下載" as downloaded +state "已安裝" as installed +state "已啓用" as enabled +state "已禁用" as disabled +state "需要更新" as needUpdate +state "已卸載" as uninstalled2 + +[*] --> uninstalled +uninstalled --> downloaded : mine-extension:download +downloaded --> installed : mine-extension:install +installed --> enabled : 自動啓用 +enabled --> disabled : 禁用插件 +disabled --> enabled : 啓用插件 +enabled --> needUpdate : 檢測到新版本 +needUpdate --> enabled : mine-extension:update +enabled --> uninstalled2 : mine-extension:uninstall +disabled --> uninstalled2 : mine-extension:uninstall +uninstalled2 --> [*] + +note right of installed : 執行 InstallScript +note right of uninstalled2 : 執行 UninstallScript + +@enduml +``` + +## 插件發現與加載 + +### 1. 插件發現機制 + +**核心實現**: `Plugin::init()` 方法在 `bin/hyperf.php` ([GitHub](https://github.com/mineadmin/mineadmin/blob/master/bin/hyperf.php)) 中調用 + +```plantuml +@startuml +participant "Application" as app +participant "Plugin::init()" as plugin +participant "ConfigProvider" as config +participant "Hyperf Container" as container + +app -> plugin : 應用啓動時調用 +plugin -> plugin : 掃描 plugin/ 目錄 +plugin -> plugin : 讀取 mine.json 配置 +plugin -> config : 加載 ConfigProvider +config -> container : 註冊服務到容器 +container -> app : 服務可用 + +note right of plugin : 只加載已安裝的插件\n(存在 install.lock 文件) + +@enduml +``` + +### 2. 加載過程詳解 + +1. **掃描插件目錄**: 遍歷 `plugin/` 目錄下的所有子目錄 +2. **檢查安裝狀態**: 驗證是否存在 `install.lock` 文件 +3. **讀取配置**: 解析 `mine.json` 配置文件 +4. **加載 ConfigProvider**: 註冊插件服務到 Hyperf 容器 +5. **註冊路由**: 自動註冊控制器路由 +6. **加載中間件**: 註冊插件中間件 +7. **註冊事件監聽器**: 加載事件監聽器 + +## 下載階段 + +### 命令使用 + +```bash +# 下載指定插件 +php bin/hyperf.php mine-extension:download --name plugin-name + +# 查看可下載的插件列表 +php bin/hyperf.php mine-extension:list +``` + +### 下載過程 + +1. **驗證 AccessToken**: 檢查 `MINE_ACCESS_TOKEN` 環境變量 +2. **請求遠程倉庫**: 從 MineAdmin 官方倉庫獲取插件信息 +3. **下載插件包**: 下載壓縮包到本地臨時目錄 +4. **解壓文件**: 解壓到 `plugin/vendor/plugin-name/` 目錄 +5. **驗證完整性**: 檢查 `mine.json` 文件是否存在且格式正確 + +### 實現原理 + +**核心服務**: App-Store 組件 ([GitHub](https://github.com/mineadmin/appstore)) 提供下載功能 + +```php +// 偽代碼示例 +class DownloadService +{ + public function download(string $pluginName): bool + { + // 1. 驗證訪問令牌 + $this->validateAccessToken(); + + // 2. 獲取插件信息 + $pluginInfo = $this->getPluginInfo($pluginName); + + // 3. 下載插件包 + $packagePath = $this->downloadPackage($pluginInfo['download_url']); + + // 4. 解壓到目標目錄 + $this->extractPackage($packagePath, $this->getPluginPath($pluginName)); + + return true; + } +} +``` + +## 安裝階段 + +### 命令使用 + +```bash +# 安裝插件 +php bin/hyperf.php mine-extension:install vendor/plugin-name --yes + +# 強制重新安裝 +php bin/hyperf.php mine-extension:install vendor/plugin-name --force +``` + +### 安裝流程詳解 + +> ⚠️ **重要提示**: 配置文件發佈、環境檢測和數據庫遷移應在 `InstallScript` 中處理,而不是依賴 ConfigProvider 的 publish 功能。 + +```plantuml +@startuml +start + +:檢查插件目錄; +if (目錄存在?) then (是) + :讀取 mine.json 配置; + if (配置有效?) then (是) + :檢查依賴關係; + if (依賴滿足?) then (是) + :安裝 Composer 依賴; + :複製前端文件; + #pink:執行 InstallScript; + note right + InstallScript 中處理: + - 環境檢測 + - 配置文件發佈 + - 數據庫遷移 + - 初始數據填充 + end note + if (InstallScript 成功?) then (是) + :創建 install.lock; + :註冊到插件列表; + :觸發安裝事件; + :清理緩存; + stop + else (失敗) + :回滾操作; + :清理臨時文件; + stop + endif + else (不滿足) + :提示安裝依賴插件; + stop + endif + else (無效) + :報告配置錯誤; + stop + endif +else (否) + :報告插件不存在; + stop +endif + +@enduml +``` + +### 1. 前置檢查 + +```php +// 安裝前檢查邏輯 +class InstallChecker +{ + public function check(string $pluginPath): array + { + $errors = []; + + // 檢查插件目錄 + if (!is_dir($pluginPath)) { + $errors[] = '插件目錄不存在'; + } + + // 檢查 mine.json + $configPath = $pluginPath . '/mine.json'; + if (!file_exists($configPath)) { + $errors[] = 'mine.json 配置文件不存在'; + } + + // 檢查依賴關係 + $config = json_decode(file_get_contents($configPath), true); + foreach ($config['require'] ?? [] as $dependency => $version) { + if (!$this->isDependencyMet($dependency, $version)) { + $errors[] = "依賴 {$dependency} 版本 {$version} 不滿足"; + } + } + + return $errors; + } +} +``` + +### 2. Composer 依賴安裝 + +安裝過程會處理插件的 Composer 依賴: + +```json +// mine.json 中的 composer 配置 +{ + "composer": { + "require": { + "hyperf/async-queue": "^3.0", + "symfony/console": "^6.0" + }, + "psr-4": { + "Plugin\\Vendor\\PluginName\\": "src" + } + } +} +``` + +系統會自動執行: +```bash +composer require hyperf/async-queue:^3.0 symfony/console:^6.0 +``` + +### 3. InstallScript 處理 ⭐ + +> **最佳實踐**: 數據庫遷移、配置發佈和環境檢測應在 `InstallScript` 中處理: + +```php +// 在 InstallScript 中處理所有安裝邏輯 +class InstallScript +{ + public function handle(): bool + { + // 1. 環境檢測 + if (!$this->checkEnvironment()) { + echo "環境不滿足要求\n"; + return false; + } + + // 2. 發佈配置文件(不使用 ConfigProvider 的 publish) + $this->publishConfig(); + + // 3. 執行數據庫遷移 + if (!$this->runMigrations()) { + echo "數據庫遷移失敗\n"; + return false; + } + + // 4. 初始化數據 + $this->seedData(); + + return true; + } + + private function publishConfig(): void + { + $source = __DIR__ . '/../publish/config/plugin.php'; + $target = BASE_PATH . '/config/autoload/plugin.php'; + + if (!file_exists($target)) { + copy($source, $target); + echo "配置文件已發佈\n"; + } + } + + private function runMigrations(): bool + { + $migrationPath = __DIR__ . '/../Database/Migrations'; + + if (is_dir($migrationPath)) { + // 使用 Hyperf 的遷移命令 + $container = \Hyperf\Context\ApplicationContext::getContainer(); + $application = $container->get(\Hyperf\Contract\ApplicationInterface::class); + + $input = new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'migrate', + '--path' => $migrationPath, + ]); + + $output = new \Symfony\Component\Console\Output\BufferedOutput(); + $exitCode = $application->run($input, $output); + + return $exitCode === 0; + } + + return true; + } +} +``` + +### 4. 前端文件複製 + +將 `web/` 目錄下的文件複製到前端項目: + +``` +plugin/vendor/plugin-name/web/ → 前端項目對應目錄 +├── views/example.vue → src/views/plugin/vendor/plugin-name/example.vue +├── components/ExampleComp.vue → src/components/plugin/vendor/plugin-name/ExampleComp.vue +└── api/example.js → src/api/plugin/vendor/plugin-name/example.js +``` + +### 5. 配置文件發佈 ⚠️ + +> **注意**: ConfigProvider 中的 `publish` 功能在插件系統中不可靠,應在 InstallScript 中手動處理: + +```php +// 不推薦:ConfigProvider 中的 publish 可能不生效 +'publish' => [ + // 這種方式在插件中可能不會執行 +] + +// 推薦:在 InstallScript 中手動發佈 +protected function publishConfig(): void +{ + $configs = [ + [ + 'source' => __DIR__ . '/../publish/config/plugin.php', + 'target' => BASE_PATH . '/config/autoload/plugin.php', + ], + [ + 'source' => __DIR__ . '/../publish/config/routes.php', + 'target' => BASE_PATH . '/config/routes/plugin.php', + ], + ]; + + foreach ($configs as $config) { + if (!file_exists($config['target'])) { + copy($config['source'], $config['target']); + echo "配置文件已發佈: {$config['target']}\n"; + } + } +} +``` + +### 6. 創建安裝鎖文件 + +安裝成功後創建 `install.lock` 文件標記安裝狀態: + +``` +plugin/vendor/plugin-name/install.lock +``` + +文件內容包含安裝信息: +```json +{ + "installed_at": "2024-01-01 12:00:00", + "version": "1.0.0", + "installer": "admin", + "checksum": "abc123..." +} +``` + +## 啓用/禁用管理 + +### 插件狀態控制 + +MineAdmin 支持在不卸載插件的情況下臨時禁用插件: + +```bash +# 禁用插件 +php bin/hyperf.php mine-extension:disable vendor/plugin-name + +# 啓用插件 +php bin/hyperf.php mine-extension:enable vendor/plugin-name + +# 查看插件狀態 +php bin/hyperf.php mine-extension:status vendor/plugin-name +``` + +### 狀態管理機制 + +狀態信息存儲在 `install.lock` 文件中: + +```json +{ + "installed_at": "2024-01-01 12:00:00", + "version": "1.0.0", + "status": "enabled", // enabled | disabled + "disabled_at": null, + "disabled_reason": null +} +``` + +## 更新階段 + +### 更新檢查 + +```bash +# 檢查插件更新 +php bin/hyperf.php mine-extension:check-updates + +# 更新指定插件 +php bin/hyperf.php mine-extension:update vendor/plugin-name + +# 更新所有插件 +php bin/hyperf.php mine-extension:update-all +``` + +### 更新流程 + +```plantuml +@startuml +start + +:檢查遠程版本; +if (有新版本?) then (是) + :備份當前插件; + :下載新版本; + :驗證完整性; + :執行更新前腳本; + :替換插件文件; + :執行數據庫遷移; + :更新配置文件; + :執行更新後腳本; + if (更新成功?) then (是) + :更新版本信息; + :清理備份; + :觸發更新事件; + stop + else (失敗) + :恢復備份; + :報告錯誤; + stop + endif +else (否) + :無需更新; + stop +endif + +@enduml +``` + +### 版本兼容性處理 + +更新時會檢查版本兼容性: + +```php +class UpdateManager +{ + public function checkCompatibility(string $currentVersion, string $newVersion): bool + { + // 檢查主版本兼容性 + $current = $this->parseVersion($currentVersion); + $new = $this->parseVersion($newVersion); + + // 主版本不同時可能存在破壞性更新 + if ($current['major'] !== $new['major']) { + return $this->checkBreakingChanges($currentVersion, $newVersion); + } + + return true; + } +} +``` + +## 卸載階段 + +### 命令使用 + +```bash +# 卸載插件 +php bin/hyperf.php mine-extension:uninstall vendor/plugin-name --yes + +# 強制卸載 (忽略錯誤) +php bin/hyperf.php mine-extension:uninstall vendor/plugin-name --force +``` + +### 卸載流程 + +```plantuml +@startuml +start + +:檢查插件狀態; +if (插件已安裝?) then (是) + :檢查依賴關係; + if (有其他插件依賴?) then (是) + :提示依賴衝突; + if (強制卸載?) then (是) + :繼續卸載; + else (否) + :取消卸載; + stop + endif + endif + + :執行 UninstallScript; + :刪除數據庫表; + :清理配置文件; + :刪除前端文件; + :清理緩存; + :移除 Composer 依賴; + :刪除插件目錄; + :清理註冊信息; + :觸發卸載事件; + stop +else (否) + :插件未安裝; + stop +endif + +@enduml +``` + +### 卸載腳本執行 + +```php +// UninstallScript 示例 +class UninstallScript +{ + public function handle(): bool + { + try { + // 1. 清理數據庫 + $this->cleanDatabase(); + + // 2. 清理配置文件 + $this->cleanConfigFiles(); + + // 3. 清理緩存數據 + $this->cleanCache(); + + // 4. 清理日誌文件 + $this->cleanLogs(); + + // 5. 執行自定義清理邏輯 + $this->customCleanup(); + + return true; + } catch (\Exception $e) { + logger()->error('插件卸載失敗: ' . $e->getMessage()); + return false; + } + } + + private function cleanDatabase(): void + { + // 刪除插件相關表 + DB::statement('DROP TABLE IF EXISTS plugin_example'); + + // 清理配置數據 + DB::table('system_config')->where('key', 'like', 'plugin.example.%')->delete(); + } +} +``` + +## 錯誤處理與回滾 + +### 安裝錯誤回滾 + +如果安裝過程中出現錯誤,系統會自動回滾: + +```php +class InstallRollback +{ + public function rollback(string $pluginPath, array $operations): void + { + foreach (array_reverse($operations) as $operation) { + try { + switch ($operation['type']) { + case 'database': + $this->rollbackDatabase($operation['data']); + break; + case 'files': + $this->rollbackFiles($operation['data']); + break; + case 'config': + $this->rollbackConfig($operation['data']); + break; + } + } catch (\Exception $e) { + logger()->error('回滾操作失敗: ' . $e->getMessage()); + } + } + } +} +``` + +### 依賴衝突處理 + +當插件之間存在依賴衝突時的處理策略: + +```php +class DependencyResolver +{ + public function resolveConflicts(array $conflicts): array + { + $solutions = []; + + foreach ($conflicts as $conflict) { + $solution = match($conflict['type']) { + 'version_conflict' => $this->resolveVersionConflict($conflict), + 'circular_dependency' => $this->resolveCircularDependency($conflict), + 'missing_dependency' => $this->resolveMissingDependency($conflict), + default => null + }; + + if ($solution) { + $solutions[] = $solution; + } + } + + return $solutions; + } +} +``` + +## 事件系統 + +插件生命週期的各個階段都會觸發相應事件: + +### 事件列表 + +```php +// 插件生命週期事件 +class PluginEvents +{ + const BEFORE_INSTALL = 'plugin.before_install'; + const AFTER_INSTALL = 'plugin.after_install'; + const BEFORE_UNINSTALL = 'plugin.before_uninstall'; + const AFTER_UNINSTALL = 'plugin.after_uninstall'; + const BEFORE_UPDATE = 'plugin.before_update'; + const AFTER_UPDATE = 'plugin.after_update'; + const ENABLED = 'plugin.enabled'; + const DISABLED = 'plugin.disabled'; +} +``` + +### 事件監聽器示例 + +```php +use Hyperf\Event\Annotation\Listener; +use Hyperf\Event\Contract\ListenerInterface; + +#[Listener] +class PluginInstallListener implements ListenerInterface +{ + public function listen(): array + { + return [ + PluginEvents::AFTER_INSTALL, + ]; + } + + public function process(object $event): void + { + // 插件安裝後的處理邏輯 + logger()->info('插件安裝完成', [ + 'plugin' => $event->getPluginName(), + 'version' => $event->getVersion() + ]); + + // 清理緩存 + $this->clearCache($event->getPluginName()); + + // 發送通知 + $this->sendNotification($event); + } +} +``` + +## 狀態查詢 + +### 查看插件狀態 + +```bash +# 查看所有本地插件狀態 +php bin/hyperf.php mine-extension:local-list + +# 查看遠程可用插件 +php bin/hyperf.php mine-extension:list + +# 查看特定插件詳情 +php bin/hyperf.php mine-extension:info vendor/plugin-name +``` + +### 狀態信息結構 + +```json +{ + "name": "vendor/plugin-name", + "version": "1.0.0", + "status": "enabled", + "installed_at": "2024-01-01 12:00:00", + "last_updated": "2024-01-15 10:30:00", + "dependencies": [ + "vendor/dependency-plugin" + ], + "dependents": [ + "vendor/dependent-plugin" + ], + "file_integrity": "valid", + "database_status": "migrated" +} +``` + +## 最佳實踐 + +### 1. 安裝腳本設計 + +- 實現冪等性:多次執行結果一致 +- 提供詳細的錯誤信息 +- 支持事務回滾 +- 記錄操作日誌 + +### 2. 卸載腳本設計 + +- 完全清理插件數據 +- 保留用户重要數據的備份選項 +- 處理依賴關係 +- 優雅降級 + +### 3. 版本管理 + +- 遵循語義化版本規範 +- 提供升級路徑説明 +- 標註破壞性更新 +- 維護更新日誌 + +## 相關文檔 + +- [插件開發指南](./develop.md) - 開發流程 +- [插件結構説明](./structure.md) - 目錄結構 +- [API 參考](./api.md) - 接口文檔 +- [示例代碼](./examples.md) - 實踐案例 \ No newline at end of file diff --git a/docs/zh-hk/plugin/structure.md b/docs/zh-hk/plugin/structure.md index ddd0429..dc4df31 100644 --- a/docs/zh-hk/plugin/structure.md +++ b/docs/zh-hk/plugin/structure.md @@ -1,20 +1,471 @@ # 插件目錄結構 -一個標準插件目錄結構説明 - ---- - -以[插件創建章節](./create.md)為例 - -```shell -- plugin/test/demo 插件根目錄 --- plugin/test/demo/src 插件後端目錄 ---- plugin/test/demo/src/InstallScript.php 插件安裝時執行類方法 ---- plugin/test/demo/src/UninstallScript.php 插件卸載時執行類方法 ---- plugin/test/demo/src/ConfigProvider.php 插件配置目錄,此文件與 hyperf 官方配置一致 --- plugin/test/demo/Database 插件數據庫遷移與填充文件目錄 ---- plugin/test/demo/Database/Migrations 插件數據庫遷移文件 ---- plugin/test/demo/Database/Seeder 插件數據庫數據填充文件 --- plugin/test/demo/web 插件前端目錄 --- plugin/test/demo/mine.json 插件核心信息文件 -``` \ No newline at end of file +詳細介紹 MineAdmin 插件的標準目錄結構、文件規範和組織方式。 + +## 標準目錄結構 + +一個完整的 MineAdmin 插件目錄結構如下: + +``` +plugin/vendor/plugin-name/ # 插件根目錄 +├── mine.json # 插件核心配置文件 ⭐ +├── README.md # 插件説明文檔 +├── LICENSE # 許可證文件 +├── composer.json # Composer 依賴配置 (可選) +├── src/ # 後端源碼目錄 ⭐ +│ ├── ConfigProvider.php # 配置提供者 ⭐ +│ ├── InstallScript.php # 安裝腳本 ⭐ +│ ├── UninstallScript.php # 卸載腳本 ⭐ +│ ├── Controller/ # 控制器目錄 +│ │ ├── AdminController.php # 管理員控制器 +│ │ └── ApiController.php # API 控制器 +│ ├── Service/ # 服務層目錄 +│ │ └── ExampleService.php # 業務服務類 +│ ├── Repository/ # 倉庫層目錄 +│ │ └── ExampleRepository.php # 數據倉庫類 +│ ├── Model/ # 模型目錄 +│ │ └── Example.php # 數據模型 +│ ├── Request/ # 請求驗證目錄 +│ │ ├── CreateRequest.php # 創建請求驗證 +│ │ └── UpdateRequest.php # 更新請求驗證 +│ ├── Resource/ # 資源轉換目錄 +│ │ └── ExampleResource.php # 資源轉換類 +│ ├── Middleware/ # 中間件目錄 +│ │ └── ExampleMiddleware.php # 自定義中間件 +│ ├── Command/ # 命令行目錄 +│ │ └── ExampleCommand.php # 自定義命令 +│ ├── Listener/ # 事件監聽器目錄 +│ │ └── ExampleListener.php # 事件監聽器 +│ └── Exception/ # 異常處理目錄 +│ └── ExampleException.php # 自定義異常 +├── web/ # 前端源碼目錄 ⭐ +│ ├── views/ # 頁面組件目錄 +│ │ ├── index.vue # 主頁面 +│ │ ├── list.vue # 列表頁面 +│ │ └── form.vue # 表單頁面 +│ ├── components/ # 公共組件目錄 +│ │ └── ExampleComponent.vue # 通用組件 +│ ├── api/ # API 接口目錄 +│ │ └── example.js # 接口定義 +│ ├── router/ # 路由配置目錄 +│ │ └── index.js # 路由配置 +│ ├── store/ # 狀態管理目錄 +│ │ └── example.js # 狀態管理 +│ └── assets/ # 靜態資源目錄 +│ ├── images/ # 圖片資源 +│ └── styles/ # 樣式文件 +├── Database/ # 數據庫相關目錄 ⭐ +│ ├── Migrations/ # 數據庫遷移文件 +│ │ └── 2024_01_01_000000_create_example_table.php +│ └── Seeders/ # 數據填充文件 +│ └── ExampleSeeder.php # 數據填充類 +├── config/ # 配置文件目錄 +│ └── example.php # 插件配置文件 +├── publish/ # 發佈文件目錄 +│ ├── config/ # 配置文件模板 +│ │ └── example.php # 配置文件模板 +│ └── assets/ # 靜態資源模板 +├── tests/ # 測試文件目錄 +│ ├── Unit/ # 單元測試 +│ ├── Feature/ # 功能測試 +│ └── TestCase.php # 測試基類 +├── docs/ # 文檔目錄 +│ ├── installation.md # 安裝文檔 +│ ├── usage.md # 使用文檔 +│ └── api.md # API 文檔 +└── .gitignore # Git 忽略文件 +``` + +## 核心文件詳解 + +### 1. mine.json (插件配置文件) + +**文件路徑**: `mine.json` ([配置詳解](./mineJson.md)) + +插件的核心配置文件,定義插件的基本信息、依賴關係和加載配置: + +```json +{ + "name": "vendor/plugin-name", + "description": "插件描述", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "Author Name", + "email": "author@example.com", + "role": "developer" + } + ], + "keywords": ["mineadmin", "plugin"], + "homepage": "https://github.com/vendor/plugin-name", + "license": "MIT", + "require": { + "php": ">=8.1", + "hyperf/framework": "^3.0" + }, + "package": { + "dependencies": { + "vue": "^3.0", + "element-plus": "^2.0" + } + }, + "composer": { + "require": { + "hyperf/async-queue": "^3.0" + }, + "psr-4": { + "Plugin\\Vendor\\PluginName\\": "src" + }, + "config": "Plugin\\Vendor\\PluginName\\ConfigProvider" + } +} +``` + +### 2. ConfigProvider.php (配置提供者) + +**文件路徑**: `src/ConfigProvider.php` +**實現原理**: 基於 Hyperf ConfigProvider 機制 ([GitHub](https://github.com/hyperf/hyperf/blob/master/src/config-provider/src/ConfigProvider.php)) + +> ⚠️ **注意**: ConfigProvider 中的 `publish` 功能在插件系統中存在問題,建議在 InstallScript 中處理配置文件發佈。 + +```php + [], + 'annotations' => [ + 'scan' => [ + 'paths' => [__DIR__], + ], + ], + 'commands' => [], + 'listeners' => [], + // publish 功能在插件中不推薦使用 + // 請在 InstallScript 中處理配置文件發佈 + ]; + } +} +``` + +### 3. InstallScript.php (安裝腳本) ⭐ + +**文件路徑**: `src/InstallScript.php` +**調用時機**: 執行 `mine-extension:install` 命令時 +**重要性**: 推薦在此處理配置發佈、環境檢測和數據庫遷移 + +```php +checkEnvironment()) { + echo "環境檢測失敗\n"; + return false; + } + + // 2. 發佈配置文件 + $this->publishConfig(); + + // 3. 執行數據庫遷移 + $this->runMigrations(); + + // 4. 初始化數據 + $this->seedData(); + + echo "插件安裝成功\n"; + return true; + } + + protected function checkEnvironment(): bool + { + // 檢查 PHP 版本 + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + echo "PHP 版本需要 >= 8.1\n"; + return false; + } + + // 檢查必要的擴展 + $requiredExtensions = ['redis', 'pdo', 'json']; + foreach ($requiredExtensions as $ext) { + if (!extension_loaded($ext)) { + echo "缺少 PHP 擴展: {$ext}\n"; + return false; + } + } + + return true; + } + + protected function publishConfig(): void + { + $source = __DIR__ . '/../publish/config/plugin.php'; + $target = BASE_PATH . '/config/autoload/plugin.php'; + + if (!file_exists($target)) { + copy($source, $target); + echo "配置文件已發佈: {$target}\n"; + } + } + + protected function runMigrations(): void + { + $migrationPath = __DIR__ . '/../Database/Migrations'; + + if (is_dir($migrationPath)) { + // 執行遷移命令 + $container = \Hyperf\Context\ApplicationContext::getContainer(); + $application = $container->get(ApplicationInterface::class); + $application->setAutoExit(false); + + $input = new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'migrate', + '--path' => $migrationPath, + ]); + + $output = new \Symfony\Component\Console\Output\BufferedOutput(); + $application->run($input, $output); + + echo "數據庫遷移完成\n"; + } + } + + protected function seedData(): void + { + // 初始化默認數據 + // 例如創建默認配置、菜單等 + } +} +``` + +### 4. UninstallScript.php (卸載腳本) ⭐ + +**文件路徑**: `src/UninstallScript.php` +**調用時機**: 執行 `mine-extension:uninstall` 命令時 +**重要性**: 清理配置文件、數據表和相關資源 + +```php +backupData(); + + // 2. 刪除數據庫表 + $this->dropTables(); + + // 3. 清理配置文件 + $this->removeConfig(); + + // 4. 清理緩存 + $this->clearCache(); + + echo "插件卸載完成\n"; + return true; + } + + protected function backupData(): void + { + // 備份重要數據到指定目錄 + $backupPath = BASE_PATH . '/runtime/backup/plugin_' . date('YmdHis') . '.sql'; + // 實現備份邏輯 + } + + protected function dropTables(): void + { + // 刪除插件創建的數據表 + $tables = ['plugin_example_table', 'plugin_settings']; + + foreach ($tables as $table) { + if (Db::schema()->hasTable($table)) { + Db::schema()->drop($table); + echo "已刪除數據表: {$table}\n"; + } + } + } + + protected function removeConfig(): void + { + $configFile = BASE_PATH . '/config/autoload/plugin.php'; + + if (file_exists($configFile)) { + unlink($configFile); + echo "配置文件已刪除: {$configFile}\n"; + } + } + + protected function clearCache(): void + { + // 清理插件相關緩存 + $redis = \Hyperf\Context\ApplicationContext::getContainer() + ->get(\Hyperf\Redis\Redis::class); + + $redis->del('plugin:cache:*'); + echo "緩存已清理\n"; + } +} +``` + +## 目錄結構圖解 + +```plantuml +@startuml +!define FOLDER rectangle +!define FILE rectangle + +FOLDER "Plugin Root" as root { + FILE "mine.json" as config + FOLDER "src/" as src { + FILE "ConfigProvider.php" as provider + FILE "InstallScript.php" as install + FILE "UninstallScript.php" as uninstall + FOLDER "Controller/" as controller + FOLDER "Service/" as service + FOLDER "Model/" as model + } + FOLDER "web/" as web { + FOLDER "views/" as views + FOLDER "components/" as components + FOLDER "api/" as api + } + FOLDER "Database/" as database { + FOLDER "Migrations/" as migrations + FOLDER "Seeders/" as seeders + } +} + +config --> provider : 配置加載 +provider --> install : 安裝時調用 +provider --> uninstall : 卸載時調用 +web --> views : 前端頁面 +database --> migrations : 數據庫結構 +database --> seeders : 初始數據 + +@enduml +``` + +## 不同類型插件的結構差異 + +### Mixed (混合型插件) +包含完整的 `src/` 和 `web/` 目錄,提供前後端完整功能。 + +### Backend (後端插件) +只包含 `src/` 目錄,專注於提供 API 服務和業務邏輯: + +``` +plugin/vendor/backend-plugin/ +├── mine.json +├── src/ +│ ├── ConfigProvider.php +│ ├── Controller/ +│ ├── Service/ +│ └── Model/ +└── Database/ +``` + +### Frontend (前端插件) +只包含 `web/` 目錄,專注於前端界面和交互: + +``` +plugin/vendor/frontend-plugin/ +├── mine.json +├── web/ +│ ├── views/ +│ ├── components/ +│ └── assets/ +└── src/ + └── ConfigProvider.php # 最小配置 +``` + +## 命名規範 + +### 1. 目錄命名 +- 使用小寫字母和連字符:`user-management` +- 避免使用下劃線和空格 + +### 2. 文件命名 +- PHP 類文件使用 PascalCase:`UserController.php` +- Vue 組件使用 PascalCase:`UserList.vue` +- 配置文件使用小寫:`user.php` + +### 3. 命名空間規範 +遵循 PSR-4 自動加載標準: + +```php +// 插件路徑: plugin/mineadmin/user-manager/ +// 命名空間: Plugin\MineAdmin\UserManager\ +namespace Plugin\MineAdmin\UserManager\Controller; +``` + +## 文件權限和安全 + +### 1. 文件權限設置 +```bash +# 設置合適的文件權限 +find plugin/ -type f -name "*.php" -exec chmod 644 {} \; +find plugin/ -type d -exec chmod 755 {} \; +``` + +### 2. 安全注意事項 +- 敏感配置使用環境變量 +- 避免在代碼中硬編碼密鑰 +- 驗證和過濾用户輸入 +- 使用 HTTPS 傳輸敏感數據 + +## 最佳實踐 + +### 1. 文件組織 +- 按功能模塊組織代碼 +- 保持目錄結構清晰 +- 使用有意義的文件名 + +### 2. 代碼規範 +- 遵循 PSR-12 編碼標準 +- 添加適當的註釋 +- 使用類型聲明 + +### 3. 版本控制 +- 使用 `.gitignore` 排除不必要的文件 +- 創建清晰的提交信息 +- 使用語義化版本號 + +## 示例項目結構 + +查看官方插件的實際結構: + +**App-Store 插件**: MineAdmin 官方應用市場插件,展示了標準的混合型插件結構 + +## 常見問題 + +### Q: 插件目錄應該放在哪裏? +A: 插件應該放在項目根目錄的 `plugin/` 目錄下,按 `vendor/plugin-name` 格式組織。 + +### Q: 如何處理插件之間的依賴? +A: 在 `mine.json` 的 `require` 字段中聲明依賴的其他插件。 + +### Q: 前端文件安裝後放在哪裏? +A: `web/` 目錄下的文件會在安裝時複製到前端項目的對應位置。 + +### Q: 數據庫遷移文件如何執行? +A: 在 `InstallScript.php` 中調用遷移執行邏輯,或使用 Hyperf 的遷移命令。 \ No newline at end of file diff --git a/docs/zh-tw/plugin/api.md b/docs/zh-tw/plugin/api.md new file mode 100644 index 0000000..2650f74 --- /dev/null +++ b/docs/zh-tw/plugin/api.md @@ -0,0 +1,753 @@ +# API 參考文件 + +本文件詳細介紹 MineAdmin 外掛系統的所有 API 介面、命令列工具和核心類庫。 + +## 命令列 API + +### 外掛管理命令 + +#### 1. mine-extension:initial + +初始化外掛擴充套件系統。 + +```bash +php bin/hyperf.php mine-extension:initial +``` + +**功能**: +- 釋出 app-store 配置檔案 +- 初始化外掛系統配置 +- 建立必要的目錄結構 + +**實現類**: `Mine\AppStore\Command\InitialCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/InitialCommand.php)) + +#### 2. mine-extension:list + +查詢遠端外掛列表。 + +```bash +php bin/hyperf.php mine-extension:list [options] +``` + +**引數**: +| 引數 | 型別 | 預設值 | 說明 | +|------|------|--------|------| +| --type | string | all | 篩選擴充套件型別 (mixed/backend/frontend) | +| --name | string | - | 篩選副檔名稱 | +| --category | string | - | 篩選分類 | +| --author | string | - | 篩選作者 | + +**示例**: +```bash +# 檢視所有外掛 +php bin/hyperf.php mine-extension:list + +# 檢視混合型外掛 +php bin/hyperf.php mine-extension:list --type=mixed + +# 搜尋特定外掛 +php bin/hyperf.php mine-extension:list --name=user-manager +``` + +**實現類**: `Mine\AppStore\Command\ListCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/ListCommand.php)) + +#### 3. mine-extension:local-list + +查詢本地所有外掛。 + +```bash +php bin/hyperf.php mine-extension:local-list [options] +``` + +**引數**: +| 引數 | 型別 | 預設值 | 說明 | +|------|------|--------|------| +| --status | string | all | 篩選狀態 (installed/enabled/disabled) | +| --type | string | all | 篩選型別 | + +**實現類**: `Mine\AppStore\Command\LocalListCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/LocalListCommand.php)) + +#### 4. mine-extension:download + +下載遠端外掛到本地。 + +```bash +php bin/hyperf.php mine-extension:download --name=plugin-name [options] +``` + +**引數**: +| 引數 | 型別 | 必選 | 說明 | +|------|------|------|------| +| --name | string | 是 | 外掛名稱 | +| --version | string | 否 | 指定版本 | +| --force | bool | 否 | 強制覆蓋已存在的外掛 | + +**實現類**: `Mine\AppStore\Command\DownloadCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/DownloadCommand.php)) + +#### 5. mine-extension:install + +安裝指定外掛。 + +```bash +php bin/hyperf.php mine-extension:install {path} [options] +``` + +**引數**: +| 引數 | 型別 | 必選 | 說明 | +|------|------|------|------| +| path | string | 是 | 外掛路徑 (vendor/plugin-name) | +| --yes | bool | 否 | 跳過確認提示 | +| --force | bool | 否 | 強制重新安裝 | +| --skip-dependencies | bool | 否 | 跳過依賴檢查 | + +**示例**: +```bash +# 安裝外掛 +php bin/hyperf.php mine-extension:install mineadmin/user-manager --yes + +# 強制重新安裝 +php bin/hyperf.php mine-extension:install mineadmin/user-manager --force +``` + +**實現類**: `Mine\AppStore\Command\InstallCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/InstallCommand.php)) + +#### 6. mine-extension:uninstall + +解除安裝指定外掛。 + +```bash +php bin/hyperf.php mine-extension:uninstall {path} [options] +``` + +**引數**: +| 引數 | 型別 | 必選 | 說明 | +|------|------|------|------| +| path | string | 是 | 外掛路徑 | +| --yes | bool | 否 | 跳過確認提示 | +| --force | bool | 否 | 強制解除安裝 (忽略錯誤) | +| --keep-data | bool | 否 | 保留使用者資料 | + +**實現類**: `Mine\AppStore\Command\UninstallCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/UninstallCommand.php)) + +#### 7. mine-extension:create + +建立新外掛。 + +```bash +php bin/hyperf.php mine-extension:create {path} [options] +``` + +**引數**: +| 引數 | 型別 | 預設值 | 說明 | +|------|------|--------|------| +| path | string | - | 外掛路徑 (vendor/plugin-name) | +| --name | string | example | 外掛顯示名稱 | +| --type | string | mixed | 外掛型別 (mixed/backend/frontend) | +| --author | string | - | 作者名稱 | +| --description | string | - | 外掛描述 | +| --license | string | MIT | 許可證型別 | + +**示例**: +```bash +php bin/hyperf.php mine-extension:create mycompany/hello-world \ + --name "Hello World" \ + --type mixed \ + --author "Your Name" \ + --description "我的第一個外掛" +``` + +**實現類**: `Mine\AppStore\Command\CreateCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/CreateCommand.php)) + +## 核心類庫 API + +### Plugin 類 + +**檔案位置**: `Mine\AppStore\Plugin` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Plugin.php)) + +外掛系統的核心類,負責外掛的載入和管理。 + +#### Plugin::init() + +初始化外掛系統,在應用啟動時呼叫。 + +```php + [ + 'name' => 'vendor/plugin-name', + 'version' => '1.0.0', + 'path' => '/path/to/plugin', + 'config' => [...], // mine.json 配置 + 'status' => 'enabled' + ] +] +``` + +#### Plugin::isInstalled() + +檢查外掛是否已安裝。 + +```php +install('vendor/plugin-name', [ + 'force' => false, + 'skip_dependencies' => false +]); + +if ($result['success']) { + echo "安裝成功"; +} else { + echo "安裝失敗: " . $result['message']; +} +``` + +#### uninstall() + +解除安裝外掛。 + +```php +uninstall('vendor/plugin-name', [ + 'force' => false, + 'keep_data' => false +]); +``` + +#### update() + +更新外掛。 + +```php +update('vendor/plugin-name'); +``` + +### ConfigProvider 基類 + +所有外掛的 ConfigProvider 都應該遵循以下介面: + +```php + [ + InterfaceA::class => ImplementationA::class, + ], + + // 註解掃描路徑 + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + + // 命令列命令 + 'commands' => [ + CustomCommand::class, + ], + + // 事件監聽器 + 'listeners' => [ + CustomListener::class, + ], + + // 中介軟體 + 'middlewares' => [ + 'http' => [ + CustomMiddleware::class, + ], + ], + + // 配置檔案釋出 + 'publish' => [ + [ + 'id' => 'config-id', + 'description' => '配置檔案描述', + 'source' => __DIR__ . '/../publish/config.php', + 'destination' => BASE_PATH . '/config/autoload/plugin.php', + ], + ], + + // 程序配置 + 'processes' => [ + CustomProcess::class, + ], + ]; + } +} +``` + +## HTTP API + +### 外掛管理介面 + +#### 獲取外掛列表 + +```http +GET /admin/plugin/list +``` + +**請求引數**: +```json +{ + "page": 1, + "pageSize": 15, + "type": "mixed", + "status": "enabled", + "keyword": "search term" +} +``` + +**響應示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "list": [ + { + "name": "vendor/plugin-name", + "display_name": "外掛顯示名稱", + "version": "1.0.0", + "description": "外掛描述", + "author": "作者名稱", + "type": "mixed", + "status": "enabled", + "installed_at": "2024-01-01 12:00:00", + "updated_at": "2024-01-15 10:30:00" + } + ], + "total": 1 + } +} +``` + +#### 安裝外掛 + +```http +POST /admin/plugin/install +``` + +**請求引數**: +```json +{ + "name": "vendor/plugin-name", + "version": "1.0.0", + "force": false +} +``` + +**響應示例**: +```json +{ + "code": 200, + "message": "安裝成功", + "data": { + "plugin": "vendor/plugin-name", + "version": "1.0.0", + "installed_at": "2024-01-01 12:00:00" + } +} +``` + +#### 解除安裝外掛 + +```http +DELETE /admin/plugin/uninstall +``` + +**請求引數**: +```json +{ + "name": "vendor/plugin-name", + "keep_data": false +} +``` + +#### 啟用/停用外掛 + +```http +PUT /admin/plugin/toggle-status +``` + +**請求引數**: +```json +{ + "name": "vendor/plugin-name", + "status": "enabled" // enabled | disabled +} +``` + +## 事件 API + +### 外掛事件系統 + +外掛系統提供了豐富的事件鉤子,允許開發者在外掛生命週期的關鍵節點執行自定義邏輯。 + +#### 事件型別 + +```php +success) { + // 外掛安裝成功後的處理 + $this->clearCache(); + $this->sendNotification($event->pluginName); + $this->updateStatistics($event->pluginName); + } else { + // 安裝失敗的處理 + logger()->error('外掛安裝失敗', [ + 'plugin' => $event->pluginName, + 'error' => $event->error + ]); + } + } +} +``` + +## 鉤子 API + +### 外掛鉤子系統 + +MineAdmin 提供了鉤子系統,允許外掛在系統關鍵點注入自定義邏輯。 + +#### 註冊鉤子 + +```php +info('使用者嘗試登入', ['user_id' => $user->id]); + }); + + HookManager::register('user.login.after', function($user) { + // 使用者登入後的處理邏輯 + $this->recordLoginHistory($user); + }); + + return [ + // ... 其他配置 + ]; + } +} +``` + +#### 觸發鉤子 + +```php +authenticate($credentials); + + if ($result) { + // 登入後鉤子 + HookManager::trigger('user.login.after', $user); + } + + return $result; + } +} +``` + +#### 可用鉤子列表 + +| 鉤子名稱 | 觸發時機 | 引數 | +|----------|----------|------| +| `user.login.before` | 使用者登入前 | User $user | +| `user.login.after` | 使用者登入後 | User $user | +| `user.logout.before` | 使用者退出前 | User $user | +| `user.logout.after` | 使用者退出後 | User $user | +| `menu.render.before` | 選單渲染前 | array $menus | +| `menu.render.after` | 選單渲染後 | array $menus | +| `permission.check.before` | 許可權檢查前 | string $permission, User $user | +| `permission.check.after` | 許可權檢查後 | bool $result, string $permission, User $user | + +## 工具類 API + +### PluginHelper 類 + +提供外掛開發的常用工具方法。 + +```php + config +config --> provider +provider --> backend +provider --> frontend +backend --> script +frontend --> script +script --> test +test --> publish + +note right of create : mine-extension:create +note right of config : 外掛元資料配置 +note right of provider : 註冊服務和路由 +note right of backend : Controller + Service +note right of frontend : Vue3 + TypeScript +note right of script : InstallScript/UninstallScript +note right of test : 本地安裝測試 +note right of publish : 釋出到應用市場 + +@enduml +``` + +## 外掛結構規範 + +基於 `app-store` 和 `code-generator` 外掛的實際程式碼,MineAdmin 外掛有兩種典型結構: + +### 簡單外掛結構(適合純後端或簡單功能) + +``` +plugin/mine-admin/plugin-name/ +├── mine.json # 外掛配置檔案 +├── install.lock # 安裝標記(自動生成) +└── src/ + ├── ConfigProvider.php # 配置提供者 + ├── Controller/ # 控制器 + │ └── IndexController.php + └── Service/ # 服務層 + └── Service.php +``` + +### 完整外掛結構(適合複雜業務) + +``` +plugin/mine-admin/plugin-name/ +├── mine.json # 外掛配置檔案 +├── install.lock # 安裝標記(自動生成) +├── README.md # 外掛說明 +├── src/ # 後端程式碼 +│ ├── ConfigProvider.php # 配置提供者 +│ ├── InstallScript.php # 安裝指令碼 +│ ├── UninstallScript.php # 解除安裝指令碼 +│ ├── Http/ +│ │ ├── Controller/ # 控制器 +│ │ ├── Request/ # 請求驗證 +│ │ └── Vo/ # 值物件 +│ ├── Model/ # 資料模型 +│ ├── Repository/ # 倉儲層 +│ └── Service/ # 服務層 +├── web/ # 前端程式碼 +│ ├── index.ts # 外掛入口 +│ ├── api/ # API介面 +│ ├── views/ # Vue元件 +│ └── locales/ # 語言包 +├── Database/ # 資料庫 +│ ├── Migrations/ # 遷移檔案 +│ └── Seeder/ # 種子資料 +├── languages/ # 後端語言包 +│ └── zh_CN/ +└── publish/ # 釋出資源 + └── template/ # 模板檔案 +``` + +## 後端開發 + +### 1. ConfigProvider 配置提供者 + +基於 app-store 外掛的實際實現: + +```php + [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + // 依賴注入(可選) + 'dependencies' => [ + // Interface::class => Implementation::class + ], + // 命令列(可選) + 'commands' => [ + // Command::class + ], + // 中介軟體(可選) + 'middlewares' => [ + 'http' => [ + // Middleware::class + ], + ], + // 事件監聽器(可選) + 'listeners' => [ + // Listener::class + ], + ]; + } +} +``` + +### 2. 控制器開發 + +參考 app-store 的 IndexController 實現: + +```php +success( + $this->service->getAppList($this->request->all()) + ); + } + + /** + * 下載外掛 + */ + #[PostMapping("download")] + #[Permission("plugin:store:download")] + public function download(): ResponseInterface + { + $params = $this->request->all(); + $this->service->download($params); + return $this->success(); + } + + /** + * 安裝外掛 + */ + #[PostMapping("install")] + #[Permission("plugin:store:install")] + public function install(): ResponseInterface + { + $params = $this->request->all(); + $this->service->install($params); + return $this->success(); + } + + /** + * 解除安裝外掛 + */ + #[PostMapping("unInstall")] + #[Permission("plugin:store:uninstall")] + public function unInstall(): ResponseInterface + { + $params = $this->request->all(); + $this->service->unInstall($params); + return $this->success(); + } + + /** + * 本地外掛安裝列表 + */ + #[GetMapping("getInstallList")] + #[RemoteState] + public function getInstallList(): ResponseInterface + { + return $this->success( + $this->service->getLocalAppInstallList() + ); + } + + /** + * 本地上傳安裝 + */ + #[PostMapping("uploadInstall")] + #[Permission("plugin:store:uploadInstall")] + public function uploadInstall(): ResponseInterface + { + return $this->success( + $this->service->uploadLocalApp($this->request->all()) + ); + } +} +``` + +**關鍵註解說明**: +- `#[Controller]`: 定義控制器路由字首 +- `#[Auth]`: 需要登入驗證 +- `#[Permission]`: 許可權驗證 +- `#[GetMapping]`/`#[PostMapping]`: 定義路由方法 +- `#[Inject]`: 依賴注入 +- `#[RemoteState]`: 遠端狀態管理 + +### 3. 服務層開發 + +基於 app-store 的 Service 實現模式: + +```php +service->getAppList($params); + } + + /** + * 下載應用 + */ + public function download(array $params): void + { + $app = $this->service->getAppInfo($params['identifier']); + + if (empty($app['download_url'])) { + throw new MineException('該應用無法下載', 500); + } + + if (Plugin::hasLocalInstalled($params['identifier'])) { + throw new MineException('應用已經存在本地,如需重新下載,請先刪除本地應用', 500); + } + + $this->service->download($params); + } + + /** + * 安裝應用 + */ + public function install(array $params): void + { + $pluginName = $params['name']; + + if (!Plugin::hasLocal($pluginName)) { + throw new MineException('外掛不存在', 500); + } + + if (Plugin::hasLocalInstalled($pluginName)) { + throw new MineException('外掛已經安裝', 500); + } + + Plugin::forceRefreshJsonPath($pluginName); + Plugin::install($pluginName); + } + + /** + * 解除安裝應用 + */ + public function unInstall(array $params): void + { + $pluginName = $params['name']; + + if (!Plugin::hasLocalInstalled($pluginName)) { + throw new MineException('外掛未安裝', 500); + } + + Plugin::uninstall($pluginName); + } + + /** + * 獲取本地已安裝外掛列表 + */ + public function getLocalAppInstallList(): array + { + $list = []; + $plugins = Plugin::getLocalPlugins(); + + foreach ($plugins as $name => $info) { + $app = ['identifier' => $name]; + $app['name'] = $info['name'] ?? '未知'; + $app['status'] = $info['status'] ?? false; + $app['version'] = $info['version'] ?? '0.0.0'; + $app['description'] = $info['description'] ?? '暫無描述'; + $app['created_at'] = $info['created_at'] ?? ''; + $list[] = $app; + } + + return $list; + } + + /** + * 本地上傳安裝 + */ + public function uploadLocalApp(array $params): void + { + if (empty($params['path'])) { + throw new MineException('請上傳外掛包', 500); + } + + // 解壓並驗證外掛包 + $zipFile = new \ZipArchive(); + $result = $zipFile->open($params['path']); + + if ($result !== true) { + throw new MineException('外掛包解壓失敗', 500); + } + + // 獲取外掛資訊並安裝 + $mineJson = $zipFile->getFromName('mine.json'); + if (!$mineJson) { + throw new MineException('外掛包格式錯誤,缺少mine.json', 500); + } + + $config = json_decode($mineJson, true); + $pluginName = $config['name'] ?? null; + + if (!$pluginName) { + throw new MineException('外掛包配置錯誤', 500); + } + + // 解壓到外掛目錄 + $targetPath = Plugin::getPluginPath($pluginName); + $zipFile->extractTo($targetPath); + $zipFile->close(); + + // 重新整理快取並安裝 + Plugin::forceRefreshJsonPath($pluginName); + Plugin::install($pluginName); + } +} +``` + +### 4. 模型層(如需資料庫) + +參考 code-generator 外掛的模型實現: + +```php + 'boolean', + 'is_list' => 'boolean', + 'is_query' => 'boolean', + 'is_required' => 'boolean', + 'is_sort' => 'boolean', + 'is_edit' => 'boolean', + 'is_readonly' => 'boolean', + ]; +} +``` + +## 前端開發 + +### 1. 外掛入口檔案 (index.ts) + +基於 app-store 的前端實現: + +```typescript +import type { App } from 'vue' +import type { Plugin } from '#/global' + +const pluginConfig: Plugin.PluginConfig = { + install(app: App) { + // Vue外掛安裝鉤子 + console.log('app-store plugin install') + }, + config: { + enable: true, + info: { + name: 'app-store', + version: '1.0.0', + author: 'MineAdmin Team', + description: 'MineAdmin應用市場視覺化外掛' + } + }, + views: [ + { + name: 'plugin:store', + path: '/plugin/store', + meta: { + title: 'app_store.app_store', + i18n: true, + icon: 'material-symbols:app-shortcut', + type: 'M', + hidden: false, + componentPath: '/plugin/mine-admin/app-store/views/index.vue', + componentName: 'plugin:mine-admin:app-store:index', + }, + component: () => import('./views/index.vue'), + } + ], +} + +export default pluginConfig +``` + +### 2. API 介面封裝 + +```typescript +// api/app-store.ts +import { request } from '@/utils/request' + +// 獲取遠端外掛列表 +export const getAppList = (params: any) => { + return request.get('/admin/plugin/store/index', { params }) +} + +// 下載外掛 +export const downloadApp = (data: any) => { + return request.post('/admin/plugin/store/download', data) +} + +// 安裝外掛 +export const installApp = (data: any) => { + return request.post('/admin/plugin/store/install', data) +} + +// 解除安裝外掛 +export const uninstallApp = (data: any) => { + return request.post('/admin/plugin/store/unInstall', data) +} + +// 獲取本地已安裝外掛 +export const getInstalledList = () => { + return request.get('/admin/plugin/store/getInstallList') +} + +// 上傳本地外掛安裝 +export const uploadInstall = (data: any) => { + return request.post('/admin/plugin/store/uploadInstall', data) +} +``` + +### 3. Vue 元件開發 + +```vue + + + + +``` + +### 4. 國際化支援 + +```typescript +// locales/zh_CN.ts +export default { + app_store: { + app_store: '應用市場', + app_list: '應用列表', + installed: '已安裝', + install: '安裝', + uninstall: '解除安裝', + download: '下載', + upload: '上傳', + local_upload: '本地上傳', + upload_tips: '請選擇外掛包檔案(.zip格式)', + } +} +``` + +## 安裝與解除安裝指令碼 + +### InstallScript.php + +基於 code-generator 外掛的實際實現: + +```php +output = new ConsoleOutput(); + + try { + $this->info('========================================'); + $this->info('MineAdmin 程式碼生成器外掛'); + $this->info('========================================'); + $this->info('開始安裝外掛...'); + + // 1. 複製模板檔案 + $this->copyTemplates(); + + // 2. 複製語言包 + $this->copyLanguages(); + + // 3. 釋出依賴資源 + $this->publishVendor(); + + // 4. 執行資料庫遷移 + $this->runMigrations(); + + $this->info('外掛安裝成功!'); + $this->info('========================================'); + + } catch (\Throwable $e) { + $this->error('外掛安裝失敗:' . $e->getMessage()); + throw $e; + } + } + + /** + * 複製模板檔案 + */ + protected function copyTemplates(): void + { + $source = dirname(__DIR__) . '/publish/template'; + $target = BASE_PATH . '/runtime/generate/template'; + + if (!is_dir($target)) { + mkdir($target, 0755, true); + } + + Filesystem::copy($source, $target, false); + $this->info('模板檔案複製成功'); + } + + /** + * 複製語言包 + */ + protected function copyLanguages(): void + { + $source = dirname(__DIR__) . '/languages'; + $target = BASE_PATH . '/storage/languages'; + + Filesystem::copy($source, $target, false); + $this->info('語言包複製成功'); + } + + /** + * 釋出依賴包資源 + */ + protected function publishVendor(): void + { + $app = ApplicationContext::getContainer()->get(ApplicationInterface::class); + $app->setAutoExit(false); + + $input = new ArrayInput([ + 'command' => 'vendor:publish', + 'package' => 'hyperf/translation', + ]); + + $app->run($input, new NullOutput()); + $this->info('依賴資源釋出成功'); + } + + /** + * 執行資料庫遷移 + */ + protected function runMigrations(): void + { + $migrationPath = dirname(__DIR__) . '/Database/Migrations'; + + if (!is_dir($migrationPath)) { + return; + } + + $app = ApplicationContext::getContainer()->get(ApplicationInterface::class); + $app->setAutoExit(false); + + $input = new ArrayInput([ + 'command' => 'migrate', + '--path' => $migrationPath, + '--force' => true, + ]); + + $app->run($input, new NullOutput()); + $this->info('資料庫遷移執行成功'); + } +} +``` + +### UninstallScript.php + +```php +output = new ConsoleOutput(); + + $this->info('========================================'); + $this->info('即將解除安裝程式碼生成器外掛'); + $this->info('========================================'); + + try { + // 清理模板檔案 + $this->cleanTemplates(); + + // 清理語言包 + $this->cleanLanguages(); + + // 清理資料庫(可選,根據需求決定是否清理) + if ($this->confirm('是否清理資料庫表?')) { + $this->cleanDatabase(); + } + + $this->info('外掛解除安裝成功!'); + + } catch (\Throwable $e) { + $this->error('外掛解除安裝失敗:' . $e->getMessage()); + throw $e; + } + } + + protected function cleanTemplates(): void + { + $templatePath = BASE_PATH . '/runtime/generate/template'; + if (is_dir($templatePath)) { + // 遞迴刪除目錄 + $this->removeDirectory($templatePath); + $this->info('模板檔案清理成功'); + } + } + + protected function cleanLanguages(): void + { + // 清理語言包檔案 + $langFile = BASE_PATH . '/storage/languages/zh_CN/code-generator.php'; + if (file_exists($langFile)) { + unlink($langFile); + $this->info('語言包清理成功'); + } + } + + protected function cleanDatabase(): void + { + // 執行資料庫清理 + // 注意:這裡需要謹慎處理,避免誤刪使用者資料 + $this->info('資料庫表清理成功'); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + is_dir($path) ? $this->removeDirectory($path) : unlink($path); + } + rmdir($dir); + } +} +``` + +## 資料庫遷移 + +基於 code-generator 的遷移檔案: + +```php +engine = 'InnoDB'; + $table->comment('生成業務表'); + $table->bigIncrements('id')->comment('主鍵'); + $table->string('table_name', 200)->comment('表名稱'); + $table->string('table_comment', 500)->comment('表註釋'); + $table->string('module_name', 100)->comment('模組名'); + $table->string('namespace', 255)->comment('名稱空間'); + $table->string('menu_name', 100)->comment('選單名稱'); + $table->bigInteger('belong_menu_id')->nullable()->comment('所屬選單'); + $table->string('package_name', 100)->nullable()->comment('包名'); + $table->addColumn('string', 'type', ['length' => 100])->comment('生成型別'); + $table->addColumn('string', 'generate_mode', ['length' => 30])->default('1')->comment('生成方式'); + $table->addColumn('string', 'generate_menus', ['length' => 255])->nullable()->comment('生成選單列表'); + $table->addColumn('string', 'build_menu', ['length' => 10])->default('1')->comment('構建選單'); + $table->addColumn('string', 'component_type', ['length' => 30])->default('1')->comment('元件型別'); + $table->json('options')->nullable()->comment('其他配置'); + $table->bigInteger('created_by')->comment('建立者'); + $table->bigInteger('updated_by')->comment('更新者'); + $table->datetimes(); + $table->unique('table_name'); + $table->index('table_name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('setting_generate_tables'); + } +}; +``` + +## 測試與除錯 + +### 1. 本地安裝測試 + +```bash +# 建立外掛 +php bin/hyperf.php mine-extension:create mine-admin/my-plugin + +# 安裝外掛 +php bin/hyperf.php mine-extension:install mine-admin/my-plugin + +# 檢視已安裝外掛 +php bin/hyperf.php mine-extension:local-list + +# 解除安裝外掛 +php bin/hyperf.php mine-extension:uninstall mine-admin/my-plugin +``` + +### 2. 除錯技巧 + +```php +// 在服務層新增日誌 +use Hyperf\Context\ApplicationContext; +use Psr\Log\LoggerInterface; + +$logger = ApplicationContext::getContainer()->get(LoggerInterface::class); +$logger->info('除錯資訊', ['data' => $data]); + +// 使用 dd() 函式除錯 +dd($variable); + +// 使用異常丟擲除錯 +throw new \Exception('除錯資訊: ' . json_encode($data)); +``` + +### 3. 前端除錯 + +```typescript +// 在瀏覽器控制檯檢視 +console.log('除錯資訊', data) + +// 使用 Vue DevTools 除錯元件狀態 + +// 檢視網路請求 +// 使用瀏覽器的 Network 面板檢視 API 請求和響應 +``` + +## 開發最佳實踐 ⭐ + +### 1. 程式碼規範 + +- **命名規範**: + - 外掛名:`vendor/plugin-name` 格式 + - 名稱空間:`Plugin\Vendor\PluginName` + - 類名:PascalCase + - 方法名:camelCase + +- **PSR規範**: + - 遵循 PSR-4 自動載入規範 + - 遵循 PSR-12 編碼規範 + +### 2. 目錄組織原則 + +- 後端程式碼統一放在 `src/` 目錄 +- 前端程式碼統一放在 `web/` 目錄 +- 資料庫相關放在 `Database/` 目錄 +- 靜態資源放在 `publish/` 目錄 +- 語言包放在 `languages/` 和 `web/locales/` 目錄 + +### 3. 配置管理 (重要) + +- **不要依賴 ConfigProvider 的 publish 功能** +- **在 InstallScript 中處理所有檔案複製和配置釋出** +- **在 InstallScript 中執行資料庫遷移** +- **在 InstallScript 中進行環境檢測** + +### 4. 安全考慮 + +```php +// 引數驗證 +use Hyperf\Validation\Request\FormRequest; + +class StoreRequest extends FormRequest +{ + public function rules(): array + { + return [ + 'name' => 'required|string|max:100', + 'email' => 'required|email', + ]; + } +} + +// 許可權控制 +#[Permission("plugin:module:action")] +public function action() {} + +// SQL注入防護 - 使用引數繫結 +$model->where('name', '=', $name)->get(); + +// XSS防護 - 前端處理 +{{ data | escape }} +``` + +### 5. 效能最佳化 + +```php +// 使用依賴注入減少耦合 +#[Inject] +protected Service $service; + +// 使用快取 +use Hyperf\Cache\Annotation\Cacheable; + +#[Cacheable(prefix: "plugin", ttl: 3600)] +public function getData() {} + +// 路由懶載入 +component: () => import('./views/index.vue') + +// 資料庫查詢最佳化 +$query->select(['id', 'name'])->with('relation')->limit(20); +``` + +### 6. 錯誤處理 + +```php +use Mine\Exception\MineException; + +// 業務異常 +if (!$condition) { + throw new MineException('錯誤資訊', 500); +} + +// try-catch 處理 +try { + // 業務邏輯 +} catch (\Throwable $e) { + $this->logger->error('操作失敗', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + throw new MineException('操作失敗: ' . $e->getMessage()); +} +``` + +## 常見問題解決 + +### Q: 外掛安裝後無法訪問? +A: +1. 檢查 ConfigProvider 的 annotations 配置是否正確 +2. 確認控制器的 #[Controller] 註解路由字首 +3. 檢查許可權註解 #[Permission] 是否已在系統中配置 + +### Q: 配置檔案沒有釋出? +A: ConfigProvider 的 publish 功能在外掛中不可靠,請在 InstallScript 中手動處理配置釋出。 + +### Q: 資料庫遷移失敗? +A: +1. 檢查資料庫連線配置 +2. 確認遷移檔案路徑正確 +3. 檢視遷移命令的錯誤輸出 + +### Q: 前端元件不顯示? +A: +1. 檢查 web/index.ts 的路由配置 +2. 確認元件路徑正確 +3. 檢視瀏覽器控制檯錯誤資訊 + +### Q: 依賴包衝突? +A: +1. 在 mine.json 中正確配置 composer 依賴版本約束 +2. 使用 `composer update` 更新依賴 +3. 檢查與主專案的依賴相容性 + +## 相關文件 + +- [外掛結構詳解](./structure.md) +- [生命週期管理](./lifecycle.md) +- [API 參考文件](./api.md) +- [示例程式碼](./examples.md) +- [mine.json 配置](./mineJson.md) +- [ConfigProvider 說明](./configProvider.md) \ No newline at end of file diff --git a/docs/zh-tw/plugin/examples.md b/docs/zh-tw/plugin/examples.md new file mode 100644 index 0000000..f0236f4 --- /dev/null +++ b/docs/zh-tw/plugin/examples.md @@ -0,0 +1,1396 @@ +# 外掛示例程式碼 + +本文件提供完整的 MineAdmin 外掛開發示例,包括不同型別外掛的實際程式碼案例和最佳實踐。 + +## 官方外掛示例 + +### App-Store 外掛 (混合型) + +**倉庫地址**: [mineadmin/appstore](https://github.com/mineadmin/appstore) + +App-Store 是 MineAdmin 唯一的官方預設外掛,提供應用市場管理功能,展示了混合型外掛的完整實現。 + +#### 核心檔案結構 +``` +plugin/mine-admin/app-store/ +├── mine.json # 外掛配置 +├── src/ # 後端程式碼 +│ ├── ConfigProvider.php # 配置提供者 +│ ├── Controller/ # 控制器 +│ ├── Service/ # 服務層 +│ └── Command/ # 命令列 +├── web/ # 前端程式碼 +│ ├── views/ # 頁面元件 +│ └── api/ # API 介面 +└── Database/ # 資料庫 +``` + +#### mine.json 配置示例 +```json +{ + "name": "mine-admin/app-store", + "description": "MineAdmin應用市場視覺化外掛", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "MineAdmin Team", + "role": "developer" + } + ], + "keywords": ["mineadmin", "app-store", "plugin-management"], + "homepage": "https://github.com/mineadmin/appstore", + "license": "MIT", + "composer": { + "require": { + "hyperf/async-queue": "^3.0" + }, + "psr-4": { + "Plugin\\MineAdmin\\AppStore\\": "src" + }, + "config": "Plugin\\MineAdmin\\AppStore\\ConfigProvider" + } +} +``` + +#### ConfigProvider 實現 +```php + [ + // 依賴注入配置 + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'commands' => [ + Command\AppStoreCommand::class, + ], + 'listeners' => [ + Listener\PluginEventListener::class, + ], + 'publish' => [ + [ + 'id' => 'appstore-config', + 'description' => 'App Store configuration file', + 'source' => __DIR__ . '/../publish/appstore.php', + 'destination' => BASE_PATH . '/config/autoload/appstore.php', + ], + ], + ]; + } +} +``` + +## 完整外掛開發示例 + +### 1. 使用者管理外掛 (混合型) + +以下是一個完整的使用者管理外掛示例,展示如何開發一個包含前後端的混合型外掛。 + +#### mine.json 配置 +```json +{ + "name": "mycompany/user-manager", + "description": "使用者管理外掛", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "composer": { + "require": { + "hyperf/database": "^3.0", + "hyperf/validation": "^3.0" + }, + "psr-4": { + "Plugin\\MyCompany\\UserManager\\": "src" + }, + "config": "Plugin\\MyCompany\\UserManager\\ConfigProvider" + }, + "package": { + "dependencies": { + "element-plus": "^2.4.0" + } + } +} +``` + +#### 核心控制器實現 +```php +request->all(); + $result = $this->service->getPageList($params); + return $this->success($result); + } + + #[PostMapping('/user')] + public function create(): array + { + $data = $this->request->all(); + $user = $this->service->create($data); + return $this->success($user, '使用者建立成功'); + } + + #[PutMapping('/user/{id}')] + public function update(int $id): array + { + $data = $this->request->all(); + $this->service->update($id, $data); + return $this->success(null, '更新成功'); + } + + #[DeleteMapping('/user/{id}')] + public function delete(int $id): array + { + $this->service->delete($id); + return $this->success(null, '刪除成功'); + } +} +``` + +#### 服務層實現 +```php +where(function($q) use ($params) { + $q->where('username', 'like', "%{$params['keyword']}%") + ->orWhere('email', 'like', "%{$params['keyword']}%"); + }); + } + + $paginator = $query->paginate( + $params['pageSize'] ?? 15, + ['*'], + 'page', + $params['page'] ?? 1 + ); + + return [ + 'items' => $paginator->items(), + 'pageInfo' => [ + 'total' => $paginator->total(), + 'currentPage' => $paginator->currentPage(), + 'totalPage' => $paginator->lastPage() + ] + ]; + } + + public function create(array $data): User + { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + return User::create($data); + } + + public function update(int $id, array $data): bool + { + if (isset($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } + + return User::query()->where('id', $id)->update($data) > 0; + } + + public function delete(int $id): bool + { + return User::destroy($id) > 0; + } +} +``` + +### 2. 後端型外掛示例 - API 服務外掛 + +以下是一個純後端 API 服務外掛的示例。 + +#### 外掛配置 (mine.json) + +```json +{ + "name": "mycompany/api-service", + "description": "API 服務外掛", + "version": "1.0.0", + "type": "backend", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "keywords": ["api", "service"], + "license": "MIT", + "composer": { + "require": { + "guzzlehttp/guzzle": "^7.0" + }, + "psr-4": { + "Plugin\\MyCompany\\ApiService\\": "src" + }, + "config": "Plugin\\MyCompany\\ApiService\\ConfigProvider" + } +} +``` + +#### ConfigProvider 實現 + +```php + [ + Contract\ApiClientInterface::class => Service\ApiClient::class, + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'commands' => [ + Command\ApiSyncCommand::class, + ], + 'publish' => [ + [ + 'id' => 'api-service-config', + 'description' => 'API 服務配置檔案', + 'source' => __DIR__ . '/../publish/api_service.php', + 'destination' => BASE_PATH . '/config/autoload/api_service.php', + ], + ], + ]; + } +} +``` + +### 3. 前端型外掛示例 - 資料視覺化外掛 + +以下是一個純前端的資料視覺化外掛示例。 + +#### mine.json 配置 +```json +{ + "name": "mycompany/data-visualization", + "description": "資料視覺化外掛", + "version": "1.0.0", + "type": "frontend", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "package": { + "dependencies": { + "echarts": "^5.4.0", + "vue-echarts": "^6.5.0" + } + } +} +``` + +## 完整外掛開發最佳實踐 + +### 1. 目錄結構規範 + +``` +plugin/vendor-name/plugin-name/ +├── mine.json # 外掛配置檔案 +├── src/ # PHP 後端程式碼 +│ ├── ConfigProvider.php # 配置提供者 +│ ├── Controller/ # 控制器 +│ ├── Service/ # 服務層 +│ ├── Model/ # 模型 +│ ├── Command/ # 命令列 +│ ├── Listener/ # 事件監聽器 +│ └── Middleware/ # 中介軟體 +├── web/ # 前端程式碼 +│ ├── views/ # Vue 頁面元件 +│ ├── api/ # API 介面封裝 +│ ├── components/ # 公共元件 +│ └── locales/ # 國際化 +├── Database/ # 資料庫 +│ ├── Migrations/ # 遷移檔案 +│ └── Seeders/ # 資料填充 +└── publish/ # 釋出檔案 + └── config.php # 配置檔案 +``` + +### 2. 命名規範 + +- **外掛名稱**: 使用 `vendor/plugin-name` 格式 +- **名稱空間**: `Plugin\VendorName\PluginName` +- **類名**: 使用大駝峰命名法 +- **方法名**: 使用小駝峰命名法 + +### 3. 核心元件示例 + +#### 控制器示例 + +```php +request->all(); + $result = $this->service->getList($params); + return $this->success($result); + } + + #[PostMapping('/create')] + public function create(): array + { + $data = $this->request->all(); + $result = $this->service->create($data); + return $this->success($result, '建立成功'); + } + $user = $this->userService->find($id); + + if (!$user) { + return $this->error('使用者不存在', 404); + } + + return $this->success($user); + } + + /** + * 更新使用者 + */ + #[PutMapping('/users/{id:\d+}')] + public function update(int $id): array + { + $data = $this->request->all(); + + $user = $this->userService->update($id, $data); + + return $this->success($user, '使用者更新成功'); + } + + /** + * 刪除使用者 + */ + #[DeleteMapping('/users/{id:\d+}')] + public function destroy(int $id): array + { + $this->userService->delete($id); + + return $this->success([], '使用者刪除成功'); + } + + /** + * 批次匯入使用者 + */ + #[PostMapping('/users/import')] + public function import(): array + { + $file = $this->request->file('file'); + + if (!$file || !$file->isValid()) { + return $this->error('請上傳有效的檔案'); + } + + $result = $this->userService->importFromFile($file); + + return $this->success($result, '匯入完成'); + } + + /** + * 匯出使用者資料 + */ + #[GetMapping('/users/export')] + public function export(): array + { + $params = $this->request->all(); + $filePath = $this->userService->exportToFile($params); + + return $this->success(['file_path' => $filePath], '匯出成功'); + } +} +``` + +#### 4. 服務層 (src/Service/UserService.php) + +```php +repository->getList($params); + } + + /** + * 建立使用者 + */ + public function create(array $data): array + { + // 密碼加密 + if (isset($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } + + // 生成使用者頭像 + if (!isset($data['avatar'])) { + $data['avatar'] = $this->generateAvatar($data['username']); + } + + $user = $this->repository->create($data); + + // 觸發使用者建立事件 + event(new UserCreatedEvent($user)); + + return $user->toArray(); + } + + /** + * 更新使用者 + */ + public function update(int $id, array $data): array + { + // 密碼更新處理 + if (isset($data['password']) && !empty($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } else { + unset($data['password']); + } + + $user = $this->repository->update($id, $data); + + // 觸發使用者更新事件 + event(new UserUpdatedEvent($user)); + + return $user->toArray(); + } + + /** + * 從檔案匯入使用者 + */ + public function importFromFile($file): array + { + $filePath = $file->getPath() . '/' . $file->getFilename(); + + // 讀取 Excel 檔案 + $data = $this->parseExcelFile($filePath); + + $successCount = 0; + $errorCount = 0; + $errors = []; + + foreach ($data as $index => $row) { + try { + $this->create([ + 'username' => $row['username'], + 'email' => $row['email'], + 'phone' => $row['phone'] ?? null, + 'password' => $row['password'] ?? '123456', + ]); + $successCount++; + } catch (\Exception $e) { + $errorCount++; + $errors[] = "第{$index}行: " . $e->getMessage(); + } + } + + return [ + 'success_count' => $successCount, + 'error_count' => $errorCount, + 'errors' => $errors + ]; + } + + /** + * 匯出使用者到檔案 + */ + public function exportToFile(array $params = []): string + { + $users = $this->repository->getAllForExport($params); + + // 生成 Excel 檔案 + $filePath = $this->generateExcelFile($users); + + return $filePath; + } + + /** + * 生成使用者頭像 + */ + private function generateAvatar(string $username): string + { + // 使用第三方庫生成頭像 + $avatar = new \Intervention\Image\ImageManager(); + // ... 頭像生成邏輯 + + return '/uploads/avatars/' . $username . '.png'; + } + + /** + * 解析 Excel 檔案 + */ + private function parseExcelFile(string $filePath): array + { + // Excel 解析邏輯 + return []; + } + + /** + * 生成 Excel 檔案 + */ + private function generateExcelFile(array $users): string + { + // Excel 生成邏輯 + return '/tmp/users_export_' . date('YmdHis') . '.xlsx'; + } + + protected function getRepository(): string + { + return UserRepository::class; + } +} +``` + +#### 5. 資料倉庫 (src/Repository/UserRepository.php) + +```php +getModel()::query(); + + // 關鍵詞搜尋 + if (!empty($params['keyword'])) { + $query->where(function ($q) use ($params) { + $q->where('username', 'like', "%{$params['keyword']}%") + ->orWhere('email', 'like', "%{$params['keyword']}%") + ->orWhere('phone', 'like', "%{$params['keyword']}%"); + }); + } + + // 狀態篩選 + if (isset($params['status'])) { + $query->where('status', $params['status']); + } + + // 角色篩選 + if (!empty($params['role_id'])) { + $query->whereHas('roles', function ($q) use ($params) { + $q->where('id', $params['role_id']); + }); + } + + // 時間範圍篩選 + if (!empty($params['created_at'])) { + $dates = explode(' - ', $params['created_at']); + if (count($dates) === 2) { + $query->whereBetween('created_at', [ + $dates[0] . ' 00:00:00', + $dates[1] . ' 23:59:59' + ]); + } + } + + // 排序 + $query->orderBy($params['sort'] ?? 'id', $params['order'] ?? 'desc'); + + return $query->paginate($params['pageSize'] ?? 15)->toArray(); + } + + /** + * 獲取匯出資料 + */ + public function getAllForExport(array $params = []): array + { + $query = $this->getModel()::query(); + + // 應用相同的篩選條件 + // ... 篩選邏輯 + + return $query->select([ + 'id', 'username', 'email', 'phone', + 'status', 'created_at', 'updated_at' + ])->get()->toArray(); + } +} +``` + +#### 6. 模型 (src/Model/User.php) + +```php + 'integer', + 'last_login_at' => 'datetime:Y-m-d H:i:s', + 'created_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', + ]; + + /** + * 狀態常量 + */ + const STATUS_DISABLED = 0; + const STATUS_ENABLED = 1; + + /** + * 關聯角色 + */ + public function roles() + { + return $this->belongsToMany(Role::class, 'user_roles'); + } + + /** + * 獲取狀態文字 + */ + public function getStatusTextAttribute(): string + { + return match($this->status) { + self::STATUS_ENABLED => '啟用', + self::STATUS_DISABLED => '停用', + default => '未知' + }; + } + + /** + * 獲取頭像 URL + */ + public function getAvatarUrlAttribute(): string + { + if (empty($this->avatar)) { + return '/default-avatar.png'; + } + + return str_starts_with($this->avatar, 'http') + ? $this->avatar + : config('app.url') . $this->avatar; + } +} +``` + +#### 7. 前端頁面 (web/views/UserList.vue) + +```vue + + + + + +``` + +#### 8. API 介面 (web/api/user.js) + +```javascript +// web/api/user.js +import { request } from '@/utils/request' + +const API_BASE = '/user-manager' + +export default { + // 獲取使用者列表 + getList(params) { + return request({ + url: `${API_BASE}/users`, + method: 'get', + params + }) + }, + + // 建立使用者 + create(data) { + return request({ + url: `${API_BASE}/users`, + method: 'post', + data + }) + }, + + // 獲取使用者詳情 + get(id) { + return request({ + url: `${API_BASE}/users/${id}`, + method: 'get' + }) + }, + + // 更新使用者 + update(id, data) { + return request({ + url: `${API_BASE}/users/${id}`, + method: 'put', + data + }) + }, + + // 刪除使用者 + delete(id) { + return request({ + url: `${API_BASE}/users/${id}`, + method: 'delete' + }) + }, + + // 批次匯入 + import(file) { + const formData = new FormData() + formData.append('file', file) + + return request({ + url: `${API_BASE}/users/import`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + // 匯出資料 + export(params) { + return request({ + url: `${API_BASE}/users/export`, + method: 'get', + params + }) + } +} +``` + +#### 9. 資料庫遷移 (Database/Migrations/create_users_table.php) + +```php +id(); + $table->string('username', 50)->unique()->comment('使用者名稱'); + $table->string('email')->unique()->comment('郵箱'); + $table->string('phone', 20)->nullable()->comment('手機號'); + $table->string('password')->comment('密碼'); + $table->string('avatar')->nullable()->comment('頭像'); + $table->tinyInteger('status')->default(1)->comment('狀態:0停用,1啟用'); + $table->timestamp('last_login_at')->nullable()->comment('最後登入時間'); + $table->timestamps(); + + $table->index(['username']); + $table->index(['email']); + $table->index(['phone']); + $table->index(['status']); + $table->index(['created_at']); + + $table->comment('使用者管理外掛-使用者表'); + }); + } + + public function down(): void + { + Schema::dropIfExists('plugin_user_manager_users'); + } +} +``` + +#### 10. 安裝指令碼 (src/InstallScript.php) + +```php +runMigrations(); + + // 2. 初始化資料 + $this->seedData(); + + // 3. 建立必要目錄 + $this->createDirectories(); + + // 4. 初始化配置 + $this->initConfig(); + + echo "使用者管理外掛安裝成功!\n"; + return true; + } catch (\Exception $e) { + echo "安裝失敗: " . $e->getMessage() . "\n"; + return false; + } + } + + private function runMigrations(): void + { + $migrationPath = __DIR__ . '/../Database/Migrations'; + + if (!is_dir($migrationPath)) { + return; + } + + $files = glob($migrationPath . '/*.php'); + sort($files); + + foreach ($files as $file) { + require_once $file; + + $className = $this->getMigrationClassName($file); + $migration = new $className(); + + if (method_exists($migration, 'up')) { + $migration->up(); + echo "執行遷移: " . basename($file) . "\n"; + } + } + } + + private function seedData(): void + { + // 插入預設管理員使用者 + Db::table('plugin_user_manager_users')->insertOrIgnore([ + 'username' => 'admin', + 'email' => 'admin@example.com', + 'password' => password_hash('123456', PASSWORD_DEFAULT), + 'status' => 1, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + echo "初始化預設資料完成\n"; + } + + private function createDirectories(): void + { + $directories = [ + BASE_PATH . '/public/uploads/avatars', + BASE_PATH . '/storage/user-manager', + ]; + + foreach ($directories as $dir) { + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + echo "建立目錄: {$dir}\n"; + } + } + } + + private function initConfig(): void + { + $configPath = BASE_PATH . '/config/autoload/user_manager.php'; + + if (!file_exists($configPath)) { + $defaultConfig = [ + 'avatar_upload_path' => '/uploads/avatars', + 'default_password' => '123456', + 'password_reset_expire' => 3600, + 'max_login_attempts' => 5, + ]; + + file_put_contents($configPath, " [ + // 依賴注入配置 + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'publish' => [ + // 配置檔案釋出設定 + ], + ]; + } +} +``` + +### 3. 新增業務邏輯 + +建立控制器 `src/Controller/HelloController.php`: + +```php + 200, + 'message' => 'Hello from MineAdmin Plugin!', + 'data' => [ + 'plugin' => 'hello-world', + 'timestamp' => time() + ] + ]; + } +} +``` + +### 4. 前端開發 (可選) + +在 `web/` 目錄下新增前端元件: + +```vue + + + + +``` + +## 安裝和測試外掛 + +### 1. 安裝外掛 + +```bash +# 安裝外掛到系統 +php bin/hyperf.php mine-extension:install mycompany/hello-world --yes +``` + +### 2. 測試功能 + +啟動開發伺服器並測試 API: + +```bash +# 啟動服務 +php bin/hyperf.php start + +# 測試 API (新終端) +curl http://localhost:9501/hello-world/greeting +``` + +### 3. 檢查安裝狀態 + +```bash +# 檢視本地已安裝外掛 +php bin/hyperf.php mine-extension:local-list +``` + +## 外掛管理命令 + +### 常用命令總覽 + +```bash +# 檢視遠端外掛列表 +php bin/hyperf.php mine-extension:list + +# 下載遠端外掛 +php bin/hyperf.php mine-extension:download --name plugin-name + +# 安裝本地外掛 +php bin/hyperf.php mine-extension:install plugin/path --yes + +# 解除安裝外掛 +php bin/hyperf.php mine-extension:uninstall plugin/path --yes + +# 檢視本地外掛 +php bin/hyperf.php mine-extension:local-list +``` + +## 開發除錯技巧 + +### 1. 日誌除錯 + +在外掛中使用 Hyperf 日誌系統: + +```php +use Hyperf\Logger\LoggerFactory; + +$logger = $container->get(LoggerFactory::class)->get('plugin'); +$logger->info('Hello World Plugin Debug', ['data' => $someData]); +``` + +### 2. 配置熱過載 + +開發期間修改配置後需要重啟服務: + +```bash +# 重啟 Hyperf 服務 +php bin/hyperf.php start +``` + +### 3. 前端熱更新 + +如果使用 MineAdmin 前端開發環境: + +```bash +# 在前端專案目錄 +npm run dev +``` + +## 下一步 + +現在您已經建立了第一個外掛!接下來可以: + +1. [深入瞭解外掛結構](./structure.md) +2. [學習完整開發流程](./develop.md) +3. [瞭解生命週期管理](./lifecycle.md) +4. [檢視更多示例](./examples.md) + +## 常見問題 + +### Q: 外掛安裝失敗怎麼辦? +A: 檢查 `mine.json` 配置是否正確,確保 PSR-4 自動載入路徑正確。 + +### Q: 如何除錯外掛? +A: 使用 Hyperf 的日誌系統和除錯工具,檢視 `runtime/logs/` 目錄下的日誌檔案。 + +### Q: 前端元件不顯示? +A: 確保前端檔案放在 `web/` 目錄下,安裝外掛時會自動複製到前端專案。 \ No newline at end of file diff --git a/docs/zh-tw/plugin/index.md b/docs/zh-tw/plugin/index.md index bb90f0c..530afe9 100644 --- a/docs/zh-tw/plugin/index.md +++ b/docs/zh-tw/plugin/index.md @@ -1,49 +1,117 @@ -# 準備工作 +# MineAdmin 外掛系統 -::: tip -如果開發 MineAdmin 應用;首先,得熟悉 MineAdmin 和 Hyperf 框架,然後做以下的準備工作。 -::: +MineAdmin 外掛系統提供了強大的擴充套件能力,允許開發者建立可複用的功能模組,實現系統的模組化和可擴充套件性。 -## 獲取AccessToken +## 外掛系統架構 -MineAdmin不管下載外掛應用、更新外掛應用還是開發外掛應用都需要 `ACCESS_TOKEN` +MineAdmin 的外掛系統基於 Hyperf 框架的 ConfigProvider 機制,提供了完整的外掛生命週期管理和自動化部署能力。 -獲取步驟: +```plantuml +@startuml +!define RECTANGLE class -- 登入 [MineAdmin](https://www.mineadmin.com/login) 官網 -- 進入 `個人中心` 的 [_設定_](https://www.mineadmin.com/member/setting) 頁面 -- 點選檢視`我的AccessToken` +RECTANGLE "MineAdmin Core" as core { + + bin/hyperf.php + + Plugin::init() +} -::: danger +RECTANGLE "Plugin Management" as mgmt { + + App-Store Component + + Extension Commands + + Plugin Loader +} ---- - -注意 +RECTANGLE "Plugin Structure" as structure { + + mine.json (配置檔案) + + src/ (後端程式碼) + + web/ (前端程式碼) + + Database/ (資料庫) +} -請保管好自己的 AccessToken,不要洩露!!! +RECTANGLE "Official Plugins" as official { + + app-store +} ---- +core --> mgmt : 外掛初始化 +mgmt --> structure : 載入外掛 +mgmt --> official : 管理官方外掛 +structure --> core : 註冊服務 -::: +@enduml +``` + +## 核心元件 + +### 1. 外掛載入器 +- **檔案**: `bin/hyperf.php` ([GitHub](https://github.com/mineadmin/mineadmin/blob/master/bin/hyperf.php)) +- **原理**: 透過 `Plugin::init()` 方法在應用啟動時自動載入所有已安裝的外掛 +- **實現**: 掃描 `plugin/` 目錄下的所有外掛並註冊其 ConfigProvider + +### 2. App-Store 元件 +- **倉庫**: [mineadmin/appstore](https://github.com/mineadmin/appstore) +- **功能**: 提供外掛的下載、安裝、解除安裝、更新等管理功能 +- **配置**: 透過 `ConfigProvider` 註冊服務和配置 + +### 3. 外掛配置系統 +- **核心檔案**: `mine.json` +- **原理**: 定義外掛的元資料、依賴關係、安裝指令碼等資訊 +- **載入**: 在外掛安裝時解析並註冊到系統中 + +## 官方外掛 + +MineAdmin 預設提供以下官方外掛: -## 配置後端 .env 檔案 +| 外掛名稱 | 功能描述 | 倉庫地址 | +|---------|----------|----------| +| app-store | 應用市場管理外掛,提供外掛的下載、安裝、解除安裝、更新等管理功能 | [GitHub](https://github.com/mineadmin/appstore) | -開啟後端根目錄的 _.env_ 檔案,找到 **MINE_ACCESS_TOKEN** 項,把剛複製的字串貼上到 **等號** 後面 +> 注:其他外掛如程式碼生成器、定時任務管理等可透過應用市場獲取或自行開發 -```ini [.env] -APP_NAME = MineAdmin +## 外掛型別 -APP_ENV = dev +MineAdmin 支援三種類型的外掛: -# 省略... +### Mixed (混合型外掛) +包含前端和後端完整功能的外掛,提供完整的業務模組。 -MINE_ACCESS_TOKEN = 107299501236086 +### Backend (後端外掛) +僅包含後端邏輯的外掛,主要提供 API 服務和業務邏輯。 + +### Frontend (前端外掛) +僅包含前端介面的外掛,主要提供使用者介面元件。 + +## 快速開始 + +### 環境準備 + +開發 MineAdmin 外掛需要: + +1. **熟悉技術棧**:MineAdmin 和 Hyperf 框架 +2. **獲取 AccessToken**: + - 登入 [MineAdmin 官網](https://www.mineadmin.com/login) + - 進入個人中心 → [設定頁面](https://www.mineadmin.com/member/setting) + - 獲取 AccessToken + +3. **配置環境變數**: +```ini +# .env 檔案 +MINE_ACCESS_TOKEN=你的AccessToken ``` -## 申請開發者 +::: warning 注意 +請妥善保管 AccessToken,避免洩露! +::: + +### 開發者認證 -如果僅是本地開發應用並且自己使用,這種情況不需要有開發者認證許可權,你也可以分發給其他任何人。 +- **本地開發**:無需認證,可自由開發和分發 +- **應用市場釋出**:需要開發者認證,聯絡 MineAdmin 團隊開通許可權 -如果打算上架官方應用市場,需要進行開發者認證後,才可釋出你的應用,並且受到官方版權保護。 +## 相關文件 -目前還未支援線上直接申請認證,需要透過聯絡 **MineAdmin團隊成員** 給你開通開發者許可權 \ No newline at end of file +- [快速入門指南](./guide.md) - 建立第一個外掛 +- [開發指南](./develop.md) - 詳細開發流程 +- [外掛結構](./structure.md) - 目錄結構規範 +- [生命週期管理](./lifecycle.md) - 安裝解除安裝流程 +- [API 參考](./api.md) - 介面文件 +- [示例程式碼](./examples.md) - 實際案例 \ No newline at end of file diff --git a/docs/zh-tw/plugin/lifecycle.md b/docs/zh-tw/plugin/lifecycle.md new file mode 100644 index 0000000..501ae5c --- /dev/null +++ b/docs/zh-tw/plugin/lifecycle.md @@ -0,0 +1,743 @@ +# 外掛生命週期管理 + +詳細介紹 MineAdmin 外掛的生命週期管理,包括安裝、啟用、停用、更新和解除安裝的完整流程。 + +## 生命週期概覽 + +MineAdmin 外掛的生命週期包括以下幾個階段: + +```plantuml +@startuml +!define RECTANGLE class + +state "未安裝" as uninstalled +state "已下載" as downloaded +state "已安裝" as installed +state "已啟用" as enabled +state "已停用" as disabled +state "需要更新" as needUpdate +state "已解除安裝" as uninstalled2 + +[*] --> uninstalled +uninstalled --> downloaded : mine-extension:download +downloaded --> installed : mine-extension:install +installed --> enabled : 自動啟用 +enabled --> disabled : 停用外掛 +disabled --> enabled : 啟用外掛 +enabled --> needUpdate : 檢測到新版本 +needUpdate --> enabled : mine-extension:update +enabled --> uninstalled2 : mine-extension:uninstall +disabled --> uninstalled2 : mine-extension:uninstall +uninstalled2 --> [*] + +note right of installed : 執行 InstallScript +note right of uninstalled2 : 執行 UninstallScript + +@enduml +``` + +## 外掛發現與載入 + +### 1. 外掛發現機制 + +**核心實現**: `Plugin::init()` 方法在 `bin/hyperf.php` ([GitHub](https://github.com/mineadmin/mineadmin/blob/master/bin/hyperf.php)) 中呼叫 + +```plantuml +@startuml +participant "Application" as app +participant "Plugin::init()" as plugin +participant "ConfigProvider" as config +participant "Hyperf Container" as container + +app -> plugin : 應用啟動時呼叫 +plugin -> plugin : 掃描 plugin/ 目錄 +plugin -> plugin : 讀取 mine.json 配置 +plugin -> config : 載入 ConfigProvider +config -> container : 註冊服務到容器 +container -> app : 服務可用 + +note right of plugin : 只加載已安裝的外掛\n(存在 install.lock 檔案) + +@enduml +``` + +### 2. 載入過程詳解 + +1. **掃描外掛目錄**: 遍歷 `plugin/` 目錄下的所有子目錄 +2. **檢查安裝狀態**: 驗證是否存在 `install.lock` 檔案 +3. **讀取配置**: 解析 `mine.json` 配置檔案 +4. **載入 ConfigProvider**: 註冊外掛服務到 Hyperf 容器 +5. **註冊路由**: 自動註冊控制器路由 +6. **載入中介軟體**: 註冊外掛中介軟體 +7. **註冊事件監聽器**: 載入事件監聽器 + +## 下載階段 + +### 命令使用 + +```bash +# 下載指定外掛 +php bin/hyperf.php mine-extension:download --name plugin-name + +# 檢視可下載的外掛列表 +php bin/hyperf.php mine-extension:list +``` + +### 下載過程 + +1. **驗證 AccessToken**: 檢查 `MINE_ACCESS_TOKEN` 環境變數 +2. **請求遠端倉庫**: 從 MineAdmin 官方倉庫獲取外掛資訊 +3. **下載外掛包**: 下載壓縮包到本地臨時目錄 +4. **解壓檔案**: 解壓到 `plugin/vendor/plugin-name/` 目錄 +5. **驗證完整性**: 檢查 `mine.json` 檔案是否存在且格式正確 + +### 實現原理 + +**核心服務**: App-Store 元件 ([GitHub](https://github.com/mineadmin/appstore)) 提供下載功能 + +```php +// 虛擬碼示例 +class DownloadService +{ + public function download(string $pluginName): bool + { + // 1. 驗證訪問令牌 + $this->validateAccessToken(); + + // 2. 獲取外掛資訊 + $pluginInfo = $this->getPluginInfo($pluginName); + + // 3. 下載外掛包 + $packagePath = $this->downloadPackage($pluginInfo['download_url']); + + // 4. 解壓到目標目錄 + $this->extractPackage($packagePath, $this->getPluginPath($pluginName)); + + return true; + } +} +``` + +## 安裝階段 + +### 命令使用 + +```bash +# 安裝外掛 +php bin/hyperf.php mine-extension:install vendor/plugin-name --yes + +# 強制重新安裝 +php bin/hyperf.php mine-extension:install vendor/plugin-name --force +``` + +### 安裝流程詳解 + +> ⚠️ **重要提示**: 配置檔案釋出、環境檢測和資料庫遷移應在 `InstallScript` 中處理,而不是依賴 ConfigProvider 的 publish 功能。 + +```plantuml +@startuml +start + +:檢查外掛目錄; +if (目錄存在?) then (是) + :讀取 mine.json 配置; + if (配置有效?) then (是) + :檢查依賴關係; + if (依賴滿足?) then (是) + :安裝 Composer 依賴; + :複製前端檔案; + #pink:執行 InstallScript; + note right + InstallScript 中處理: + - 環境檢測 + - 配置檔案釋出 + - 資料庫遷移 + - 初始資料填充 + end note + if (InstallScript 成功?) then (是) + :建立 install.lock; + :註冊到外掛列表; + :觸發安裝事件; + :清理快取; + stop + else (失敗) + :回滾操作; + :清理臨時檔案; + stop + endif + else (不滿足) + :提示安裝依賴外掛; + stop + endif + else (無效) + :報告配置錯誤; + stop + endif +else (否) + :報告外掛不存在; + stop +endif + +@enduml +``` + +### 1. 前置檢查 + +```php +// 安裝前檢查邏輯 +class InstallChecker +{ + public function check(string $pluginPath): array + { + $errors = []; + + // 檢查外掛目錄 + if (!is_dir($pluginPath)) { + $errors[] = '外掛目錄不存在'; + } + + // 檢查 mine.json + $configPath = $pluginPath . '/mine.json'; + if (!file_exists($configPath)) { + $errors[] = 'mine.json 配置檔案不存在'; + } + + // 檢查依賴關係 + $config = json_decode(file_get_contents($configPath), true); + foreach ($config['require'] ?? [] as $dependency => $version) { + if (!$this->isDependencyMet($dependency, $version)) { + $errors[] = "依賴 {$dependency} 版本 {$version} 不滿足"; + } + } + + return $errors; + } +} +``` + +### 2. Composer 依賴安裝 + +安裝過程會處理外掛的 Composer 依賴: + +```json +// mine.json 中的 composer 配置 +{ + "composer": { + "require": { + "hyperf/async-queue": "^3.0", + "symfony/console": "^6.0" + }, + "psr-4": { + "Plugin\\Vendor\\PluginName\\": "src" + } + } +} +``` + +系統會自動執行: +```bash +composer require hyperf/async-queue:^3.0 symfony/console:^6.0 +``` + +### 3. InstallScript 處理 ⭐ + +> **最佳實踐**: 資料庫遷移、配置釋出和環境檢測應在 `InstallScript` 中處理: + +```php +// 在 InstallScript 中處理所有安裝邏輯 +class InstallScript +{ + public function handle(): bool + { + // 1. 環境檢測 + if (!$this->checkEnvironment()) { + echo "環境不滿足要求\n"; + return false; + } + + // 2. 釋出配置檔案(不使用 ConfigProvider 的 publish) + $this->publishConfig(); + + // 3. 執行資料庫遷移 + if (!$this->runMigrations()) { + echo "資料庫遷移失敗\n"; + return false; + } + + // 4. 初始化資料 + $this->seedData(); + + return true; + } + + private function publishConfig(): void + { + $source = __DIR__ . '/../publish/config/plugin.php'; + $target = BASE_PATH . '/config/autoload/plugin.php'; + + if (!file_exists($target)) { + copy($source, $target); + echo "配置檔案已釋出\n"; + } + } + + private function runMigrations(): bool + { + $migrationPath = __DIR__ . '/../Database/Migrations'; + + if (is_dir($migrationPath)) { + // 使用 Hyperf 的遷移命令 + $container = \Hyperf\Context\ApplicationContext::getContainer(); + $application = $container->get(\Hyperf\Contract\ApplicationInterface::class); + + $input = new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'migrate', + '--path' => $migrationPath, + ]); + + $output = new \Symfony\Component\Console\Output\BufferedOutput(); + $exitCode = $application->run($input, $output); + + return $exitCode === 0; + } + + return true; + } +} +``` + +### 4. 前端檔案複製 + +將 `web/` 目錄下的檔案複製到前端專案: + +``` +plugin/vendor/plugin-name/web/ → 前端專案對應目錄 +├── views/example.vue → src/views/plugin/vendor/plugin-name/example.vue +├── components/ExampleComp.vue → src/components/plugin/vendor/plugin-name/ExampleComp.vue +└── api/example.js → src/api/plugin/vendor/plugin-name/example.js +``` + +### 5. 配置檔案釋出 ⚠️ + +> **注意**: ConfigProvider 中的 `publish` 功能在外掛系統中不可靠,應在 InstallScript 中手動處理: + +```php +// 不推薦:ConfigProvider 中的 publish 可能不生效 +'publish' => [ + // 這種方式在外掛中可能不會執行 +] + +// 推薦:在 InstallScript 中手動釋出 +protected function publishConfig(): void +{ + $configs = [ + [ + 'source' => __DIR__ . '/../publish/config/plugin.php', + 'target' => BASE_PATH . '/config/autoload/plugin.php', + ], + [ + 'source' => __DIR__ . '/../publish/config/routes.php', + 'target' => BASE_PATH . '/config/routes/plugin.php', + ], + ]; + + foreach ($configs as $config) { + if (!file_exists($config['target'])) { + copy($config['source'], $config['target']); + echo "配置檔案已釋出: {$config['target']}\n"; + } + } +} +``` + +### 6. 建立安裝鎖檔案 + +安裝成功後建立 `install.lock` 檔案標記安裝狀態: + +``` +plugin/vendor/plugin-name/install.lock +``` + +檔案內容包含安裝資訊: +```json +{ + "installed_at": "2024-01-01 12:00:00", + "version": "1.0.0", + "installer": "admin", + "checksum": "abc123..." +} +``` + +## 啟用/停用管理 + +### 外掛狀態控制 + +MineAdmin 支援在不解除安裝外掛的情況下臨時停用外掛: + +```bash +# 停用外掛 +php bin/hyperf.php mine-extension:disable vendor/plugin-name + +# 啟用外掛 +php bin/hyperf.php mine-extension:enable vendor/plugin-name + +# 檢視外掛狀態 +php bin/hyperf.php mine-extension:status vendor/plugin-name +``` + +### 狀態管理機制 + +狀態資訊儲存在 `install.lock` 檔案中: + +```json +{ + "installed_at": "2024-01-01 12:00:00", + "version": "1.0.0", + "status": "enabled", // enabled | disabled + "disabled_at": null, + "disabled_reason": null +} +``` + +## 更新階段 + +### 更新檢查 + +```bash +# 檢查外掛更新 +php bin/hyperf.php mine-extension:check-updates + +# 更新指定外掛 +php bin/hyperf.php mine-extension:update vendor/plugin-name + +# 更新所有外掛 +php bin/hyperf.php mine-extension:update-all +``` + +### 更新流程 + +```plantuml +@startuml +start + +:檢查遠端版本; +if (有新版本?) then (是) + :備份當前外掛; + :下載新版本; + :驗證完整性; + :執行更新前指令碼; + :替換外掛檔案; + :執行資料庫遷移; + :更新配置檔案; + :執行更新後腳本; + if (更新成功?) then (是) + :更新版本資訊; + :清理備份; + :觸發更新事件; + stop + else (失敗) + :恢復備份; + :報告錯誤; + stop + endif +else (否) + :無需更新; + stop +endif + +@enduml +``` + +### 版本相容性處理 + +更新時會檢查版本相容性: + +```php +class UpdateManager +{ + public function checkCompatibility(string $currentVersion, string $newVersion): bool + { + // 檢查主版本相容性 + $current = $this->parseVersion($currentVersion); + $new = $this->parseVersion($newVersion); + + // 主版本不同時可能存在破壞性更新 + if ($current['major'] !== $new['major']) { + return $this->checkBreakingChanges($currentVersion, $newVersion); + } + + return true; + } +} +``` + +## 解除安裝階段 + +### 命令使用 + +```bash +# 解除安裝外掛 +php bin/hyperf.php mine-extension:uninstall vendor/plugin-name --yes + +# 強制解除安裝 (忽略錯誤) +php bin/hyperf.php mine-extension:uninstall vendor/plugin-name --force +``` + +### 解除安裝流程 + +```plantuml +@startuml +start + +:檢查外掛狀態; +if (外掛已安裝?) then (是) + :檢查依賴關係; + if (有其他外掛依賴?) then (是) + :提示依賴衝突; + if (強制解除安裝?) then (是) + :繼續解除安裝; + else (否) + :取消解除安裝; + stop + endif + endif + + :執行 UninstallScript; + :刪除資料庫表; + :清理配置檔案; + :刪除前端檔案; + :清理快取; + :移除 Composer 依賴; + :刪除外掛目錄; + :清理註冊資訊; + :觸發解除安裝事件; + stop +else (否) + :外掛未安裝; + stop +endif + +@enduml +``` + +### 解除安裝指令碼執行 + +```php +// UninstallScript 示例 +class UninstallScript +{ + public function handle(): bool + { + try { + // 1. 清理資料庫 + $this->cleanDatabase(); + + // 2. 清理配置檔案 + $this->cleanConfigFiles(); + + // 3. 清理快取資料 + $this->cleanCache(); + + // 4. 清理日誌檔案 + $this->cleanLogs(); + + // 5. 執行自定義清理邏輯 + $this->customCleanup(); + + return true; + } catch (\Exception $e) { + logger()->error('外掛解除安裝失敗: ' . $e->getMessage()); + return false; + } + } + + private function cleanDatabase(): void + { + // 刪除外掛相關表 + DB::statement('DROP TABLE IF EXISTS plugin_example'); + + // 清理配置資料 + DB::table('system_config')->where('key', 'like', 'plugin.example.%')->delete(); + } +} +``` + +## 錯誤處理與回滾 + +### 安裝錯誤回滾 + +如果安裝過程中出現錯誤,系統會自動回滾: + +```php +class InstallRollback +{ + public function rollback(string $pluginPath, array $operations): void + { + foreach (array_reverse($operations) as $operation) { + try { + switch ($operation['type']) { + case 'database': + $this->rollbackDatabase($operation['data']); + break; + case 'files': + $this->rollbackFiles($operation['data']); + break; + case 'config': + $this->rollbackConfig($operation['data']); + break; + } + } catch (\Exception $e) { + logger()->error('回滾操作失敗: ' . $e->getMessage()); + } + } + } +} +``` + +### 依賴衝突處理 + +當外掛之間存在依賴衝突時的處理策略: + +```php +class DependencyResolver +{ + public function resolveConflicts(array $conflicts): array + { + $solutions = []; + + foreach ($conflicts as $conflict) { + $solution = match($conflict['type']) { + 'version_conflict' => $this->resolveVersionConflict($conflict), + 'circular_dependency' => $this->resolveCircularDependency($conflict), + 'missing_dependency' => $this->resolveMissingDependency($conflict), + default => null + }; + + if ($solution) { + $solutions[] = $solution; + } + } + + return $solutions; + } +} +``` + +## 事件系統 + +外掛生命週期的各個階段都會觸發相應事件: + +### 事件列表 + +```php +// 外掛生命週期事件 +class PluginEvents +{ + const BEFORE_INSTALL = 'plugin.before_install'; + const AFTER_INSTALL = 'plugin.after_install'; + const BEFORE_UNINSTALL = 'plugin.before_uninstall'; + const AFTER_UNINSTALL = 'plugin.after_uninstall'; + const BEFORE_UPDATE = 'plugin.before_update'; + const AFTER_UPDATE = 'plugin.after_update'; + const ENABLED = 'plugin.enabled'; + const DISABLED = 'plugin.disabled'; +} +``` + +### 事件監聽器示例 + +```php +use Hyperf\Event\Annotation\Listener; +use Hyperf\Event\Contract\ListenerInterface; + +#[Listener] +class PluginInstallListener implements ListenerInterface +{ + public function listen(): array + { + return [ + PluginEvents::AFTER_INSTALL, + ]; + } + + public function process(object $event): void + { + // 外掛安裝後的處理邏輯 + logger()->info('外掛安裝完成', [ + 'plugin' => $event->getPluginName(), + 'version' => $event->getVersion() + ]); + + // 清理快取 + $this->clearCache($event->getPluginName()); + + // 傳送通知 + $this->sendNotification($event); + } +} +``` + +## 狀態查詢 + +### 檢視外掛狀態 + +```bash +# 檢視所有本地外掛狀態 +php bin/hyperf.php mine-extension:local-list + +# 檢視遠端可用外掛 +php bin/hyperf.php mine-extension:list + +# 檢視特定外掛詳情 +php bin/hyperf.php mine-extension:info vendor/plugin-name +``` + +### 狀態資訊結構 + +```json +{ + "name": "vendor/plugin-name", + "version": "1.0.0", + "status": "enabled", + "installed_at": "2024-01-01 12:00:00", + "last_updated": "2024-01-15 10:30:00", + "dependencies": [ + "vendor/dependency-plugin" + ], + "dependents": [ + "vendor/dependent-plugin" + ], + "file_integrity": "valid", + "database_status": "migrated" +} +``` + +## 最佳實踐 + +### 1. 安裝指令碼設計 + +- 實現冪等性:多次執行結果一致 +- 提供詳細的錯誤資訊 +- 支援事務回滾 +- 記錄操作日誌 + +### 2. 解除安裝指令碼設計 + +- 完全清理外掛資料 +- 保留使用者重要資料的備份選項 +- 處理依賴關係 +- 優雅降級 + +### 3. 版本管理 + +- 遵循語義化版本規範 +- 提供升級路徑說明 +- 標註破壞性更新 +- 維護更新日誌 + +## 相關文件 + +- [外掛開發指南](./develop.md) - 開發流程 +- [外掛結構說明](./structure.md) - 目錄結構 +- [API 參考](./api.md) - 介面文件 +- [示例程式碼](./examples.md) - 實踐案例 \ No newline at end of file diff --git a/docs/zh-tw/plugin/structure.md b/docs/zh-tw/plugin/structure.md index 75cc5a3..7104900 100644 --- a/docs/zh-tw/plugin/structure.md +++ b/docs/zh-tw/plugin/structure.md @@ -1,20 +1,471 @@ # 外掛目錄結構 -一個標準外掛目錄結構說明 - ---- - -以[外掛建立章節](./create.md)為例 - -```shell -- plugin/test/demo 外掛根目錄 --- plugin/test/demo/src 外掛後端目錄 ---- plugin/test/demo/src/InstallScript.php 外掛安裝時執行類方法 ---- plugin/test/demo/src/UninstallScript.php 外掛解除安裝時執行類方法 ---- plugin/test/demo/src/ConfigProvider.php 外掛配置目錄,此檔案與 hyperf 官方配置一致 --- plugin/test/demo/Database 外掛資料庫遷移與填充檔案目錄 ---- plugin/test/demo/Database/Migrations 外掛資料庫遷移檔案 ---- plugin/test/demo/Database/Seeder 外掛資料庫資料填充檔案 --- plugin/test/demo/web 外掛前端目錄 --- plugin/test/demo/mine.json 外掛核心資訊檔案 -``` \ No newline at end of file +詳細介紹 MineAdmin 外掛的標準目錄結構、檔案規範和組織方式。 + +## 標準目錄結構 + +一個完整的 MineAdmin 外掛目錄結構如下: + +``` +plugin/vendor/plugin-name/ # 外掛根目錄 +├── mine.json # 外掛核心配置檔案 ⭐ +├── README.md # 外掛說明文件 +├── LICENSE # 許可證檔案 +├── composer.json # Composer 依賴配置 (可選) +├── src/ # 後端原始碼目錄 ⭐ +│ ├── ConfigProvider.php # 配置提供者 ⭐ +│ ├── InstallScript.php # 安裝指令碼 ⭐ +│ ├── UninstallScript.php # 解除安裝指令碼 ⭐ +│ ├── Controller/ # 控制器目錄 +│ │ ├── AdminController.php # 管理員控制器 +│ │ └── ApiController.php # API 控制器 +│ ├── Service/ # 服務層目錄 +│ │ └── ExampleService.php # 業務服務類 +│ ├── Repository/ # 倉庫層目錄 +│ │ └── ExampleRepository.php # 資料倉庫類 +│ ├── Model/ # 模型目錄 +│ │ └── Example.php # 資料模型 +│ ├── Request/ # 請求驗證目錄 +│ │ ├── CreateRequest.php # 建立請求驗證 +│ │ └── UpdateRequest.php # 更新請求驗證 +│ ├── Resource/ # 資源轉換目錄 +│ │ └── ExampleResource.php # 資源轉換類 +│ ├── Middleware/ # 中介軟體目錄 +│ │ └── ExampleMiddleware.php # 自定義中介軟體 +│ ├── Command/ # 命令列目錄 +│ │ └── ExampleCommand.php # 自定義命令 +│ ├── Listener/ # 事件監聽器目錄 +│ │ └── ExampleListener.php # 事件監聽器 +│ └── Exception/ # 異常處理目錄 +│ └── ExampleException.php # 自定義異常 +├── web/ # 前端原始碼目錄 ⭐ +│ ├── views/ # 頁面元件目錄 +│ │ ├── index.vue # 主頁面 +│ │ ├── list.vue # 列表頁面 +│ │ └── form.vue # 表單頁面 +│ ├── components/ # 公共元件目錄 +│ │ └── ExampleComponent.vue # 通用元件 +│ ├── api/ # API 介面目錄 +│ │ └── example.js # 介面定義 +│ ├── router/ # 路由配置目錄 +│ │ └── index.js # 路由配置 +│ ├── store/ # 狀態管理目錄 +│ │ └── example.js # 狀態管理 +│ └── assets/ # 靜態資源目錄 +│ ├── images/ # 圖片資源 +│ └── styles/ # 樣式檔案 +├── Database/ # 資料庫相關目錄 ⭐ +│ ├── Migrations/ # 資料庫遷移檔案 +│ │ └── 2024_01_01_000000_create_example_table.php +│ └── Seeders/ # 資料填充檔案 +│ └── ExampleSeeder.php # 資料填充類 +├── config/ # 配置檔案目錄 +│ └── example.php # 外掛配置檔案 +├── publish/ # 釋出檔案目錄 +│ ├── config/ # 配置檔案模板 +│ │ └── example.php # 配置檔案模板 +│ └── assets/ # 靜態資源模板 +├── tests/ # 測試檔案目錄 +│ ├── Unit/ # 單元測試 +│ ├── Feature/ # 功能測試 +│ └── TestCase.php # 測試基類 +├── docs/ # 文件目錄 +│ ├── installation.md # 安裝文件 +│ ├── usage.md # 使用文件 +│ └── api.md # API 文件 +└── .gitignore # Git 忽略檔案 +``` + +## 核心檔案詳解 + +### 1. mine.json (外掛配置檔案) + +**檔案路徑**: `mine.json` ([配置詳解](./mineJson.md)) + +外掛的核心配置檔案,定義外掛的基本資訊、依賴關係和載入配置: + +```json +{ + "name": "vendor/plugin-name", + "description": "外掛描述", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "Author Name", + "email": "author@example.com", + "role": "developer" + } + ], + "keywords": ["mineadmin", "plugin"], + "homepage": "https://github.com/vendor/plugin-name", + "license": "MIT", + "require": { + "php": ">=8.1", + "hyperf/framework": "^3.0" + }, + "package": { + "dependencies": { + "vue": "^3.0", + "element-plus": "^2.0" + } + }, + "composer": { + "require": { + "hyperf/async-queue": "^3.0" + }, + "psr-4": { + "Plugin\\Vendor\\PluginName\\": "src" + }, + "config": "Plugin\\Vendor\\PluginName\\ConfigProvider" + } +} +``` + +### 2. ConfigProvider.php (配置提供者) + +**檔案路徑**: `src/ConfigProvider.php` +**實現原理**: 基於 Hyperf ConfigProvider 機制 ([GitHub](https://github.com/hyperf/hyperf/blob/master/src/config-provider/src/ConfigProvider.php)) + +> ⚠️ **注意**: ConfigProvider 中的 `publish` 功能在外掛系統中存在問題,建議在 InstallScript 中處理配置檔案釋出。 + +```php + [], + 'annotations' => [ + 'scan' => [ + 'paths' => [__DIR__], + ], + ], + 'commands' => [], + 'listeners' => [], + // publish 功能在外掛中不推薦使用 + // 請在 InstallScript 中處理配置檔案釋出 + ]; + } +} +``` + +### 3. InstallScript.php (安裝指令碼) ⭐ + +**檔案路徑**: `src/InstallScript.php` +**呼叫時機**: 執行 `mine-extension:install` 命令時 +**重要性**: 推薦在此處理配置釋出、環境檢測和資料庫遷移 + +```php +checkEnvironment()) { + echo "環境檢測失敗\n"; + return false; + } + + // 2. 釋出配置檔案 + $this->publishConfig(); + + // 3. 執行資料庫遷移 + $this->runMigrations(); + + // 4. 初始化資料 + $this->seedData(); + + echo "外掛安裝成功\n"; + return true; + } + + protected function checkEnvironment(): bool + { + // 檢查 PHP 版本 + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + echo "PHP 版本需要 >= 8.1\n"; + return false; + } + + // 檢查必要的擴充套件 + $requiredExtensions = ['redis', 'pdo', 'json']; + foreach ($requiredExtensions as $ext) { + if (!extension_loaded($ext)) { + echo "缺少 PHP 擴充套件: {$ext}\n"; + return false; + } + } + + return true; + } + + protected function publishConfig(): void + { + $source = __DIR__ . '/../publish/config/plugin.php'; + $target = BASE_PATH . '/config/autoload/plugin.php'; + + if (!file_exists($target)) { + copy($source, $target); + echo "配置檔案已釋出: {$target}\n"; + } + } + + protected function runMigrations(): void + { + $migrationPath = __DIR__ . '/../Database/Migrations'; + + if (is_dir($migrationPath)) { + // 執行遷移命令 + $container = \Hyperf\Context\ApplicationContext::getContainer(); + $application = $container->get(ApplicationInterface::class); + $application->setAutoExit(false); + + $input = new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'migrate', + '--path' => $migrationPath, + ]); + + $output = new \Symfony\Component\Console\Output\BufferedOutput(); + $application->run($input, $output); + + echo "資料庫遷移完成\n"; + } + } + + protected function seedData(): void + { + // 初始化預設資料 + // 例如建立預設配置、選單等 + } +} +``` + +### 4. UninstallScript.php (解除安裝指令碼) ⭐ + +**檔案路徑**: `src/UninstallScript.php` +**呼叫時機**: 執行 `mine-extension:uninstall` 命令時 +**重要性**: 清理配置檔案、資料表和相關資源 + +```php +backupData(); + + // 2. 刪除資料庫表 + $this->dropTables(); + + // 3. 清理配置檔案 + $this->removeConfig(); + + // 4. 清理快取 + $this->clearCache(); + + echo "外掛解除安裝完成\n"; + return true; + } + + protected function backupData(): void + { + // 備份重要資料到指定目錄 + $backupPath = BASE_PATH . '/runtime/backup/plugin_' . date('YmdHis') . '.sql'; + // 實現備份邏輯 + } + + protected function dropTables(): void + { + // 刪除外掛建立的資料表 + $tables = ['plugin_example_table', 'plugin_settings']; + + foreach ($tables as $table) { + if (Db::schema()->hasTable($table)) { + Db::schema()->drop($table); + echo "已刪除資料表: {$table}\n"; + } + } + } + + protected function removeConfig(): void + { + $configFile = BASE_PATH . '/config/autoload/plugin.php'; + + if (file_exists($configFile)) { + unlink($configFile); + echo "配置檔案已刪除: {$configFile}\n"; + } + } + + protected function clearCache(): void + { + // 清理外掛相關快取 + $redis = \Hyperf\Context\ApplicationContext::getContainer() + ->get(\Hyperf\Redis\Redis::class); + + $redis->del('plugin:cache:*'); + echo "快取已清理\n"; + } +} +``` + +## 目錄結構圖解 + +```plantuml +@startuml +!define FOLDER rectangle +!define FILE rectangle + +FOLDER "Plugin Root" as root { + FILE "mine.json" as config + FOLDER "src/" as src { + FILE "ConfigProvider.php" as provider + FILE "InstallScript.php" as install + FILE "UninstallScript.php" as uninstall + FOLDER "Controller/" as controller + FOLDER "Service/" as service + FOLDER "Model/" as model + } + FOLDER "web/" as web { + FOLDER "views/" as views + FOLDER "components/" as components + FOLDER "api/" as api + } + FOLDER "Database/" as database { + FOLDER "Migrations/" as migrations + FOLDER "Seeders/" as seeders + } +} + +config --> provider : 配置載入 +provider --> install : 安裝時呼叫 +provider --> uninstall : 解除安裝時呼叫 +web --> views : 前端頁面 +database --> migrations : 資料庫結構 +database --> seeders : 初始資料 + +@enduml +``` + +## 不同型別外掛的結構差異 + +### Mixed (混合型外掛) +包含完整的 `src/` 和 `web/` 目錄,提供前後端完整功能。 + +### Backend (後端外掛) +只包含 `src/` 目錄,專注於提供 API 服務和業務邏輯: + +``` +plugin/vendor/backend-plugin/ +├── mine.json +├── src/ +│ ├── ConfigProvider.php +│ ├── Controller/ +│ ├── Service/ +│ └── Model/ +└── Database/ +``` + +### Frontend (前端外掛) +只包含 `web/` 目錄,專注於前端介面和互動: + +``` +plugin/vendor/frontend-plugin/ +├── mine.json +├── web/ +│ ├── views/ +│ ├── components/ +│ └── assets/ +└── src/ + └── ConfigProvider.php # 最小配置 +``` + +## 命名規範 + +### 1. 目錄命名 +- 使用小寫字母和連字元:`user-management` +- 避免使用下劃線和空格 + +### 2. 檔案命名 +- PHP 類檔案使用 PascalCase:`UserController.php` +- Vue 元件使用 PascalCase:`UserList.vue` +- 配置檔案使用小寫:`user.php` + +### 3. 名稱空間規範 +遵循 PSR-4 自動載入標準: + +```php +// 外掛路徑: plugin/mineadmin/user-manager/ +// 名稱空間: Plugin\MineAdmin\UserManager\ +namespace Plugin\MineAdmin\UserManager\Controller; +``` + +## 檔案許可權和安全 + +### 1. 檔案許可權設定 +```bash +# 設定合適的檔案許可權 +find plugin/ -type f -name "*.php" -exec chmod 644 {} \; +find plugin/ -type d -exec chmod 755 {} \; +``` + +### 2. 安全注意事項 +- 敏感配置使用環境變數 +- 避免在程式碼中硬編碼金鑰 +- 驗證和過濾使用者輸入 +- 使用 HTTPS 傳輸敏感資料 + +## 最佳實踐 + +### 1. 檔案組織 +- 按功能模組組織程式碼 +- 保持目錄結構清晰 +- 使用有意義的檔名 + +### 2. 程式碼規範 +- 遵循 PSR-12 編碼標準 +- 新增適當的註釋 +- 使用型別宣告 + +### 3. 版本控制 +- 使用 `.gitignore` 排除不必要的檔案 +- 建立清晰的提交資訊 +- 使用語義化版本號 + +## 示例專案結構 + +檢視官方外掛的實際結構: + +**App-Store 外掛**: MineAdmin 官方應用市場外掛,展示了標準的混合型外掛結構 + +## 常見問題 + +### Q: 外掛目錄應該放在哪裡? +A: 外掛應該放在專案根目錄的 `plugin/` 目錄下,按 `vendor/plugin-name` 格式組織。 + +### Q: 如何處理外掛之間的依賴? +A: 在 `mine.json` 的 `require` 欄位中宣告依賴的其他外掛。 + +### Q: 前端檔案安裝後放在哪裡? +A: `web/` 目錄下的檔案會在安裝時複製到前端專案的對應位置。 + +### Q: 資料庫遷移檔案如何執行? +A: 在 `InstallScript.php` 中呼叫遷移執行邏輯,或使用 Hyperf 的遷移命令。 \ No newline at end of file diff --git a/docs/zh/plugin/api.md b/docs/zh/plugin/api.md new file mode 100644 index 0000000..98c89c0 --- /dev/null +++ b/docs/zh/plugin/api.md @@ -0,0 +1,753 @@ +# API 参考文档 + +本文档详细介绍 MineAdmin 插件系统的所有 API 接口、命令行工具和核心类库。 + +## 命令行 API + +### 插件管理命令 + +#### 1. mine-extension:initial + +初始化插件扩展系统。 + +```bash +php bin/hyperf.php mine-extension:initial +``` + +**功能**: +- 发布 app-store 配置文件 +- 初始化插件系统配置 +- 创建必要的目录结构 + +**实现类**: `Mine\AppStore\Command\InitialCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/InitialCommand.php)) + +#### 2. mine-extension:list + +查询远程插件列表。 + +```bash +php bin/hyperf.php mine-extension:list [options] +``` + +**参数**: +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| --type | string | all | 筛选扩展类型 (mixed/backend/frontend) | +| --name | string | - | 筛选扩展名称 | +| --category | string | - | 筛选分类 | +| --author | string | - | 筛选作者 | + +**示例**: +```bash +# 查看所有插件 +php bin/hyperf.php mine-extension:list + +# 查看混合型插件 +php bin/hyperf.php mine-extension:list --type=mixed + +# 搜索特定插件 +php bin/hyperf.php mine-extension:list --name=user-manager +``` + +**实现类**: `Mine\AppStore\Command\ListCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/ListCommand.php)) + +#### 3. mine-extension:local-list + +查询本地所有插件。 + +```bash +php bin/hyperf.php mine-extension:local-list [options] +``` + +**参数**: +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| --status | string | all | 筛选状态 (installed/enabled/disabled) | +| --type | string | all | 筛选类型 | + +**实现类**: `Mine\AppStore\Command\LocalListCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/LocalListCommand.php)) + +#### 4. mine-extension:download + +下载远程插件到本地。 + +```bash +php bin/hyperf.php mine-extension:download --name=plugin-name [options] +``` + +**参数**: +| 参数 | 类型 | 必选 | 说明 | +|------|------|------|------| +| --name | string | 是 | 插件名称 | +| --version | string | 否 | 指定版本 | +| --force | bool | 否 | 强制覆盖已存在的插件 | + +**实现类**: `Mine\AppStore\Command\DownloadCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/DownloadCommand.php)) + +#### 5. mine-extension:install + +安装指定插件。 + +```bash +php bin/hyperf.php mine-extension:install {path} [options] +``` + +**参数**: +| 参数 | 类型 | 必选 | 说明 | +|------|------|------|------| +| path | string | 是 | 插件路径 (vendor/plugin-name) | +| --yes | bool | 否 | 跳过确认提示 | +| --force | bool | 否 | 强制重新安装 | +| --skip-dependencies | bool | 否 | 跳过依赖检查 | + +**示例**: +```bash +# 安装插件 +php bin/hyperf.php mine-extension:install mineadmin/user-manager --yes + +# 强制重新安装 +php bin/hyperf.php mine-extension:install mineadmin/user-manager --force +``` + +**实现类**: `Mine\AppStore\Command\InstallCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/InstallCommand.php)) + +#### 6. mine-extension:uninstall + +卸载指定插件。 + +```bash +php bin/hyperf.php mine-extension:uninstall {path} [options] +``` + +**参数**: +| 参数 | 类型 | 必选 | 说明 | +|------|------|------|------| +| path | string | 是 | 插件路径 | +| --yes | bool | 否 | 跳过确认提示 | +| --force | bool | 否 | 强制卸载 (忽略错误) | +| --keep-data | bool | 否 | 保留用户数据 | + +**实现类**: `Mine\AppStore\Command\UninstallCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/UninstallCommand.php)) + +#### 7. mine-extension:create + +创建新插件。 + +```bash +php bin/hyperf.php mine-extension:create {path} [options] +``` + +**参数**: +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| path | string | - | 插件路径 (vendor/plugin-name) | +| --name | string | example | 插件显示名称 | +| --type | string | mixed | 插件类型 (mixed/backend/frontend) | +| --author | string | - | 作者名称 | +| --description | string | - | 插件描述 | +| --license | string | MIT | 许可证类型 | + +**示例**: +```bash +php bin/hyperf.php mine-extension:create mycompany/hello-world \ + --name "Hello World" \ + --type mixed \ + --author "Your Name" \ + --description "我的第一个插件" +``` + +**实现类**: `Mine\AppStore\Command\CreateCommand` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Command/CreateCommand.php)) + +## 核心类库 API + +### Plugin 类 + +**文件位置**: `Mine\AppStore\Plugin` ([GitHub](https://github.com/mineadmin/appstore/blob/3.0/src/Plugin.php)) + +插件系统的核心类,负责插件的加载和管理。 + +#### Plugin::init() + +初始化插件系统,在应用启动时调用。 + +```php + [ + 'name' => 'vendor/plugin-name', + 'version' => '1.0.0', + 'path' => '/path/to/plugin', + 'config' => [...], // mine.json 配置 + 'status' => 'enabled' + ] +] +``` + +#### Plugin::isInstalled() + +检查插件是否已安装。 + +```php +install('vendor/plugin-name', [ + 'force' => false, + 'skip_dependencies' => false +]); + +if ($result['success']) { + echo "安装成功"; +} else { + echo "安装失败: " . $result['message']; +} +``` + +#### uninstall() + +卸载插件。 + +```php +uninstall('vendor/plugin-name', [ + 'force' => false, + 'keep_data' => false +]); +``` + +#### update() + +更新插件。 + +```php +update('vendor/plugin-name'); +``` + +### ConfigProvider 基类 + +所有插件的 ConfigProvider 都应该遵循以下接口: + +```php + [ + InterfaceA::class => ImplementationA::class, + ], + + // 注解扫描路径 + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + + // 命令行命令 + 'commands' => [ + CustomCommand::class, + ], + + // 事件监听器 + 'listeners' => [ + CustomListener::class, + ], + + // 中间件 + 'middlewares' => [ + 'http' => [ + CustomMiddleware::class, + ], + ], + + // 配置文件发布 + 'publish' => [ + [ + 'id' => 'config-id', + 'description' => '配置文件描述', + 'source' => __DIR__ . '/../publish/config.php', + 'destination' => BASE_PATH . '/config/autoload/plugin.php', + ], + ], + + // 进程配置 + 'processes' => [ + CustomProcess::class, + ], + ]; + } +} +``` + +## HTTP API + +### 插件管理接口 + +#### 获取插件列表 + +```http +GET /admin/plugin/list +``` + +**请求参数**: +```json +{ + "page": 1, + "pageSize": 15, + "type": "mixed", + "status": "enabled", + "keyword": "search term" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "success", + "data": { + "list": [ + { + "name": "vendor/plugin-name", + "display_name": "插件显示名称", + "version": "1.0.0", + "description": "插件描述", + "author": "作者名称", + "type": "mixed", + "status": "enabled", + "installed_at": "2024-01-01 12:00:00", + "updated_at": "2024-01-15 10:30:00" + } + ], + "total": 1 + } +} +``` + +#### 安装插件 + +```http +POST /admin/plugin/install +``` + +**请求参数**: +```json +{ + "name": "vendor/plugin-name", + "version": "1.0.0", + "force": false +} +``` + +**响应示例**: +```json +{ + "code": 200, + "message": "安装成功", + "data": { + "plugin": "vendor/plugin-name", + "version": "1.0.0", + "installed_at": "2024-01-01 12:00:00" + } +} +``` + +#### 卸载插件 + +```http +DELETE /admin/plugin/uninstall +``` + +**请求参数**: +```json +{ + "name": "vendor/plugin-name", + "keep_data": false +} +``` + +#### 启用/禁用插件 + +```http +PUT /admin/plugin/toggle-status +``` + +**请求参数**: +```json +{ + "name": "vendor/plugin-name", + "status": "enabled" // enabled | disabled +} +``` + +## 事件 API + +### 插件事件系统 + +插件系统提供了丰富的事件钩子,允许开发者在插件生命周期的关键节点执行自定义逻辑。 + +#### 事件类型 + +```php +success) { + // 插件安装成功后的处理 + $this->clearCache(); + $this->sendNotification($event->pluginName); + $this->updateStatistics($event->pluginName); + } else { + // 安装失败的处理 + logger()->error('插件安装失败', [ + 'plugin' => $event->pluginName, + 'error' => $event->error + ]); + } + } +} +``` + +## 钩子 API + +### 插件钩子系统 + +MineAdmin 提供了钩子系统,允许插件在系统关键点注入自定义逻辑。 + +#### 注册钩子 + +```php +info('用户尝试登录', ['user_id' => $user->id]); + }); + + HookManager::register('user.login.after', function($user) { + // 用户登录后的处理逻辑 + $this->recordLoginHistory($user); + }); + + return [ + // ... 其他配置 + ]; + } +} +``` + +#### 触发钩子 + +```php +authenticate($credentials); + + if ($result) { + // 登录后钩子 + HookManager::trigger('user.login.after', $user); + } + + return $result; + } +} +``` + +#### 可用钩子列表 + +| 钩子名称 | 触发时机 | 参数 | +|----------|----------|------| +| `user.login.before` | 用户登录前 | User $user | +| `user.login.after` | 用户登录后 | User $user | +| `user.logout.before` | 用户退出前 | User $user | +| `user.logout.after` | 用户退出后 | User $user | +| `menu.render.before` | 菜单渲染前 | array $menus | +| `menu.render.after` | 菜单渲染后 | array $menus | +| `permission.check.before` | 权限检查前 | string $permission, User $user | +| `permission.check.after` | 权限检查后 | bool $result, string $permission, User $user | + +## 工具类 API + +### PluginHelper 类 + +提供插件开发的常用工具方法。 + +```php + config +config --> provider +provider --> backend +provider --> frontend +backend --> script +frontend --> script +script --> test +test --> publish + +note right of create : mine-extension:create +note right of config : 插件元数据配置 +note right of provider : 注册服务和路由 +note right of backend : Controller + Service +note right of frontend : Vue3 + TypeScript +note right of script : InstallScript/UninstallScript +note right of test : 本地安装测试 +note right of publish : 发布到应用市场 + +@enduml +``` + +## 插件结构规范 + +基于 `app-store` 和 `code-generator` 插件的实际代码,MineAdmin 插件有两种典型结构: + +### 简单插件结构(适合纯后端或简单功能) + +``` +plugin/mine-admin/plugin-name/ +├── mine.json # 插件配置文件 +├── install.lock # 安装标记(自动生成) +└── src/ + ├── ConfigProvider.php # 配置提供者 + ├── Controller/ # 控制器 + │ └── IndexController.php + └── Service/ # 服务层 + └── Service.php +``` + +### 完整插件结构(适合复杂业务) + +``` +plugin/mine-admin/plugin-name/ +├── mine.json # 插件配置文件 +├── install.lock # 安装标记(自动生成) +├── README.md # 插件说明 +├── src/ # 后端代码 +│ ├── ConfigProvider.php # 配置提供者 +│ ├── InstallScript.php # 安装脚本 +│ ├── UninstallScript.php # 卸载脚本 +│ ├── Http/ +│ │ ├── Controller/ # 控制器 +│ │ ├── Request/ # 请求验证 +│ │ └── Vo/ # 值对象 +│ ├── Model/ # 数据模型 +│ ├── Repository/ # 仓储层 +│ └── Service/ # 服务层 +├── web/ # 前端代码 +│ ├── index.ts # 插件入口 +│ ├── api/ # API接口 +│ ├── views/ # Vue组件 +│ └── locales/ # 语言包 +├── Database/ # 数据库 +│ ├── Migrations/ # 迁移文件 +│ └── Seeder/ # 种子数据 +├── languages/ # 后端语言包 +│ └── zh_CN/ +└── publish/ # 发布资源 + └── template/ # 模板文件 +``` + +## 后端开发 + +### 1. ConfigProvider 配置提供者 + +基于 app-store 插件的实际实现: + +```php + [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + // 依赖注入(可选) + 'dependencies' => [ + // Interface::class => Implementation::class + ], + // 命令行(可选) + 'commands' => [ + // Command::class + ], + // 中间件(可选) + 'middlewares' => [ + 'http' => [ + // Middleware::class + ], + ], + // 事件监听器(可选) + 'listeners' => [ + // Listener::class + ], + ]; + } +} +``` + +### 2. 控制器开发 + +参考 app-store 的 IndexController 实现: + +```php +success( + $this->service->getAppList($this->request->all()) + ); + } + + /** + * 下载插件 + */ + #[PostMapping("download")] + #[Permission("plugin:store:download")] + public function download(): ResponseInterface + { + $params = $this->request->all(); + $this->service->download($params); + return $this->success(); + } + + /** + * 安装插件 + */ + #[PostMapping("install")] + #[Permission("plugin:store:install")] + public function install(): ResponseInterface + { + $params = $this->request->all(); + $this->service->install($params); + return $this->success(); + } + + /** + * 卸载插件 + */ + #[PostMapping("unInstall")] + #[Permission("plugin:store:uninstall")] + public function unInstall(): ResponseInterface + { + $params = $this->request->all(); + $this->service->unInstall($params); + return $this->success(); + } + + /** + * 本地插件安装列表 + */ + #[GetMapping("getInstallList")] + #[RemoteState] + public function getInstallList(): ResponseInterface + { + return $this->success( + $this->service->getLocalAppInstallList() + ); + } + + /** + * 本地上传安装 + */ + #[PostMapping("uploadInstall")] + #[Permission("plugin:store:uploadInstall")] + public function uploadInstall(): ResponseInterface + { + return $this->success( + $this->service->uploadLocalApp($this->request->all()) + ); + } +} +``` + +**关键注解说明**: +- `#[Controller]`: 定义控制器路由前缀 +- `#[Auth]`: 需要登录验证 +- `#[Permission]`: 权限验证 +- `#[GetMapping]`/`#[PostMapping]`: 定义路由方法 +- `#[Inject]`: 依赖注入 +- `#[RemoteState]`: 远程状态管理 + +### 3. 服务层开发 + +基于 app-store 的 Service 实现模式: + +```php +service->getAppList($params); + } + + /** + * 下载应用 + */ + public function download(array $params): void + { + $app = $this->service->getAppInfo($params['identifier']); + + if (empty($app['download_url'])) { + throw new MineException('该应用无法下载', 500); + } + + if (Plugin::hasLocalInstalled($params['identifier'])) { + throw new MineException('应用已经存在本地,如需重新下载,请先删除本地应用', 500); + } + + $this->service->download($params); + } + + /** + * 安装应用 + */ + public function install(array $params): void + { + $pluginName = $params['name']; + + if (!Plugin::hasLocal($pluginName)) { + throw new MineException('插件不存在', 500); + } + + if (Plugin::hasLocalInstalled($pluginName)) { + throw new MineException('插件已经安装', 500); + } + + Plugin::forceRefreshJsonPath($pluginName); + Plugin::install($pluginName); + } + + /** + * 卸载应用 + */ + public function unInstall(array $params): void + { + $pluginName = $params['name']; + + if (!Plugin::hasLocalInstalled($pluginName)) { + throw new MineException('插件未安装', 500); + } + + Plugin::uninstall($pluginName); + } + + /** + * 获取本地已安装插件列表 + */ + public function getLocalAppInstallList(): array + { + $list = []; + $plugins = Plugin::getLocalPlugins(); + + foreach ($plugins as $name => $info) { + $app = ['identifier' => $name]; + $app['name'] = $info['name'] ?? '未知'; + $app['status'] = $info['status'] ?? false; + $app['version'] = $info['version'] ?? '0.0.0'; + $app['description'] = $info['description'] ?? '暂无描述'; + $app['created_at'] = $info['created_at'] ?? ''; + $list[] = $app; + } + + return $list; + } + + /** + * 本地上传安装 + */ + public function uploadLocalApp(array $params): void + { + if (empty($params['path'])) { + throw new MineException('请上传插件包', 500); + } + + // 解压并验证插件包 + $zipFile = new \ZipArchive(); + $result = $zipFile->open($params['path']); + + if ($result !== true) { + throw new MineException('插件包解压失败', 500); + } + + // 获取插件信息并安装 + $mineJson = $zipFile->getFromName('mine.json'); + if (!$mineJson) { + throw new MineException('插件包格式错误,缺少mine.json', 500); + } + + $config = json_decode($mineJson, true); + $pluginName = $config['name'] ?? null; + + if (!$pluginName) { + throw new MineException('插件包配置错误', 500); + } + + // 解压到插件目录 + $targetPath = Plugin::getPluginPath($pluginName); + $zipFile->extractTo($targetPath); + $zipFile->close(); + + // 刷新缓存并安装 + Plugin::forceRefreshJsonPath($pluginName); + Plugin::install($pluginName); + } +} +``` + +### 4. 模型层(如需数据库) + +参考 code-generator 插件的模型实现: + +```php + 'boolean', + 'is_list' => 'boolean', + 'is_query' => 'boolean', + 'is_required' => 'boolean', + 'is_sort' => 'boolean', + 'is_edit' => 'boolean', + 'is_readonly' => 'boolean', + ]; +} +``` + +## 前端开发 + +### 1. 插件入口文件 (index.ts) + +基于 app-store 的前端实现: + +```typescript +import type { App } from 'vue' +import type { Plugin } from '#/global' + +const pluginConfig: Plugin.PluginConfig = { + install(app: App) { + // Vue插件安装钩子 + console.log('app-store plugin install') + }, + config: { + enable: true, + info: { + name: 'app-store', + version: '1.0.0', + author: 'MineAdmin Team', + description: 'MineAdmin应用市场可视化插件' + } + }, + views: [ + { + name: 'plugin:store', + path: '/plugin/store', + meta: { + title: 'app_store.app_store', + i18n: true, + icon: 'material-symbols:app-shortcut', + type: 'M', + hidden: false, + componentPath: '/plugin/mine-admin/app-store/views/index.vue', + componentName: 'plugin:mine-admin:app-store:index', + }, + component: () => import('./views/index.vue'), + } + ], +} + +export default pluginConfig +``` + +### 2. API 接口封装 + +```typescript +// api/app-store.ts +import { request } from '@/utils/request' + +// 获取远程插件列表 +export const getAppList = (params: any) => { + return request.get('/admin/plugin/store/index', { params }) +} + +// 下载插件 +export const downloadApp = (data: any) => { + return request.post('/admin/plugin/store/download', data) +} + +// 安装插件 +export const installApp = (data: any) => { + return request.post('/admin/plugin/store/install', data) +} + +// 卸载插件 +export const uninstallApp = (data: any) => { + return request.post('/admin/plugin/store/unInstall', data) +} + +// 获取本地已安装插件 +export const getInstalledList = () => { + return request.get('/admin/plugin/store/getInstallList') +} + +// 上传本地插件安装 +export const uploadInstall = (data: any) => { + return request.post('/admin/plugin/store/uploadInstall', data) +} +``` + +### 3. Vue 组件开发 + +```vue + + + + +``` + +### 4. 国际化支持 + +```typescript +// locales/zh_CN.ts +export default { + app_store: { + app_store: '应用市场', + app_list: '应用列表', + installed: '已安装', + install: '安装', + uninstall: '卸载', + download: '下载', + upload: '上传', + local_upload: '本地上传', + upload_tips: '请选择插件包文件(.zip格式)', + } +} +``` + +## 安装与卸载脚本 + +### InstallScript.php + +基于 code-generator 插件的实际实现: + +```php +output = new ConsoleOutput(); + + try { + $this->info('========================================'); + $this->info('MineAdmin 代码生成器插件'); + $this->info('========================================'); + $this->info('开始安装插件...'); + + // 1. 复制模板文件 + $this->copyTemplates(); + + // 2. 复制语言包 + $this->copyLanguages(); + + // 3. 发布依赖资源 + $this->publishVendor(); + + // 4. 执行数据库迁移 + $this->runMigrations(); + + $this->info('插件安装成功!'); + $this->info('========================================'); + + } catch (\Throwable $e) { + $this->error('插件安装失败:' . $e->getMessage()); + throw $e; + } + } + + /** + * 复制模板文件 + */ + protected function copyTemplates(): void + { + $source = dirname(__DIR__) . '/publish/template'; + $target = BASE_PATH . '/runtime/generate/template'; + + if (!is_dir($target)) { + mkdir($target, 0755, true); + } + + Filesystem::copy($source, $target, false); + $this->info('模板文件复制成功'); + } + + /** + * 复制语言包 + */ + protected function copyLanguages(): void + { + $source = dirname(__DIR__) . '/languages'; + $target = BASE_PATH . '/storage/languages'; + + Filesystem::copy($source, $target, false); + $this->info('语言包复制成功'); + } + + /** + * 发布依赖包资源 + */ + protected function publishVendor(): void + { + $app = ApplicationContext::getContainer()->get(ApplicationInterface::class); + $app->setAutoExit(false); + + $input = new ArrayInput([ + 'command' => 'vendor:publish', + 'package' => 'hyperf/translation', + ]); + + $app->run($input, new NullOutput()); + $this->info('依赖资源发布成功'); + } + + /** + * 执行数据库迁移 + */ + protected function runMigrations(): void + { + $migrationPath = dirname(__DIR__) . '/Database/Migrations'; + + if (!is_dir($migrationPath)) { + return; + } + + $app = ApplicationContext::getContainer()->get(ApplicationInterface::class); + $app->setAutoExit(false); + + $input = new ArrayInput([ + 'command' => 'migrate', + '--path' => $migrationPath, + '--force' => true, + ]); + + $app->run($input, new NullOutput()); + $this->info('数据库迁移执行成功'); + } +} +``` + +### UninstallScript.php + +```php +output = new ConsoleOutput(); + + $this->info('========================================'); + $this->info('即将卸载代码生成器插件'); + $this->info('========================================'); + + try { + // 清理模板文件 + $this->cleanTemplates(); + + // 清理语言包 + $this->cleanLanguages(); + + // 清理数据库(可选,根据需求决定是否清理) + if ($this->confirm('是否清理数据库表?')) { + $this->cleanDatabase(); + } + + $this->info('插件卸载成功!'); + + } catch (\Throwable $e) { + $this->error('插件卸载失败:' . $e->getMessage()); + throw $e; + } + } + + protected function cleanTemplates(): void + { + $templatePath = BASE_PATH . '/runtime/generate/template'; + if (is_dir($templatePath)) { + // 递归删除目录 + $this->removeDirectory($templatePath); + $this->info('模板文件清理成功'); + } + } + + protected function cleanLanguages(): void + { + // 清理语言包文件 + $langFile = BASE_PATH . '/storage/languages/zh_CN/code-generator.php'; + if (file_exists($langFile)) { + unlink($langFile); + $this->info('语言包清理成功'); + } + } + + protected function cleanDatabase(): void + { + // 执行数据库清理 + // 注意:这里需要谨慎处理,避免误删用户数据 + $this->info('数据库表清理成功'); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + is_dir($path) ? $this->removeDirectory($path) : unlink($path); + } + rmdir($dir); + } +} +``` + +## 数据库迁移 + +基于 code-generator 的迁移文件: + +```php +engine = 'InnoDB'; + $table->comment('生成业务表'); + $table->bigIncrements('id')->comment('主键'); + $table->string('table_name', 200)->comment('表名称'); + $table->string('table_comment', 500)->comment('表注释'); + $table->string('module_name', 100)->comment('模块名'); + $table->string('namespace', 255)->comment('命名空间'); + $table->string('menu_name', 100)->comment('菜单名称'); + $table->bigInteger('belong_menu_id')->nullable()->comment('所属菜单'); + $table->string('package_name', 100)->nullable()->comment('包名'); + $table->addColumn('string', 'type', ['length' => 100])->comment('生成类型'); + $table->addColumn('string', 'generate_mode', ['length' => 30])->default('1')->comment('生成方式'); + $table->addColumn('string', 'generate_menus', ['length' => 255])->nullable()->comment('生成菜单列表'); + $table->addColumn('string', 'build_menu', ['length' => 10])->default('1')->comment('构建菜单'); + $table->addColumn('string', 'component_type', ['length' => 30])->default('1')->comment('组件类型'); + $table->json('options')->nullable()->comment('其他配置'); + $table->bigInteger('created_by')->comment('创建者'); + $table->bigInteger('updated_by')->comment('更新者'); + $table->datetimes(); + $table->unique('table_name'); + $table->index('table_name'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('setting_generate_tables'); + } +}; +``` + +## 测试与调试 + +### 1. 本地安装测试 + +```bash +# 创建插件 +php bin/hyperf.php mine-extension:create mine-admin/my-plugin + +# 安装插件 +php bin/hyperf.php mine-extension:install mine-admin/my-plugin + +# 查看已安装插件 +php bin/hyperf.php mine-extension:local-list + +# 卸载插件 +php bin/hyperf.php mine-extension:uninstall mine-admin/my-plugin +``` + +### 2. 调试技巧 + +```php +// 在服务层添加日志 +use Hyperf\Context\ApplicationContext; +use Psr\Log\LoggerInterface; + +$logger = ApplicationContext::getContainer()->get(LoggerInterface::class); +$logger->info('调试信息', ['data' => $data]); + +// 使用 dd() 函数调试 +dd($variable); + +// 使用异常抛出调试 +throw new \Exception('调试信息: ' . json_encode($data)); +``` + +### 3. 前端调试 + +```typescript +// 在浏览器控制台查看 +console.log('调试信息', data) + +// 使用 Vue DevTools 调试组件状态 + +// 查看网络请求 +// 使用浏览器的 Network 面板查看 API 请求和响应 +``` + +## 开发最佳实践 ⭐ + +### 1. 代码规范 + +- **命名规范**: + - 插件名:`vendor/plugin-name` 格式 + - 命名空间:`Plugin\Vendor\PluginName` + - 类名:PascalCase + - 方法名:camelCase + +- **PSR规范**: + - 遵循 PSR-4 自动加载规范 + - 遵循 PSR-12 编码规范 + +### 2. 目录组织原则 + +- 后端代码统一放在 `src/` 目录 +- 前端代码统一放在 `web/` 目录 +- 数据库相关放在 `Database/` 目录 +- 静态资源放在 `publish/` 目录 +- 语言包放在 `languages/` 和 `web/locales/` 目录 + +### 3. 配置管理 (重要) + +- **不要依赖 ConfigProvider 的 publish 功能** +- **在 InstallScript 中处理所有文件复制和配置发布** +- **在 InstallScript 中执行数据库迁移** +- **在 InstallScript 中进行环境检测** + +### 4. 安全考虑 + +```php +// 参数验证 +use Hyperf\Validation\Request\FormRequest; + +class StoreRequest extends FormRequest +{ + public function rules(): array + { + return [ + 'name' => 'required|string|max:100', + 'email' => 'required|email', + ]; + } +} + +// 权限控制 +#[Permission("plugin:module:action")] +public function action() {} + +// SQL注入防护 - 使用参数绑定 +$model->where('name', '=', $name)->get(); + +// XSS防护 - 前端处理 +{{ data | escape }} +``` + +### 5. 性能优化 + +```php +// 使用依赖注入减少耦合 +#[Inject] +protected Service $service; + +// 使用缓存 +use Hyperf\Cache\Annotation\Cacheable; + +#[Cacheable(prefix: "plugin", ttl: 3600)] +public function getData() {} + +// 路由懒加载 +component: () => import('./views/index.vue') + +// 数据库查询优化 +$query->select(['id', 'name'])->with('relation')->limit(20); +``` + +### 6. 错误处理 + +```php +use Mine\Exception\MineException; + +// 业务异常 +if (!$condition) { + throw new MineException('错误信息', 500); +} + +// try-catch 处理 +try { + // 业务逻辑 +} catch (\Throwable $e) { + $this->logger->error('操作失败', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + throw new MineException('操作失败: ' . $e->getMessage()); +} +``` + +## 常见问题解决 + +### Q: 插件安装后无法访问? +A: +1. 检查 ConfigProvider 的 annotations 配置是否正确 +2. 确认控制器的 #[Controller] 注解路由前缀 +3. 检查权限注解 #[Permission] 是否已在系统中配置 + +### Q: 配置文件没有发布? +A: ConfigProvider 的 publish 功能在插件中不可靠,请在 InstallScript 中手动处理配置发布。 + +### Q: 数据库迁移失败? +A: +1. 检查数据库连接配置 +2. 确认迁移文件路径正确 +3. 查看迁移命令的错误输出 + +### Q: 前端组件不显示? +A: +1. 检查 web/index.ts 的路由配置 +2. 确认组件路径正确 +3. 查看浏览器控制台错误信息 + +### Q: 依赖包冲突? +A: +1. 在 mine.json 中正确配置 composer 依赖版本约束 +2. 使用 `composer update` 更新依赖 +3. 检查与主项目的依赖兼容性 + +## 相关文档 + +- [插件结构详解](./structure.md) +- [生命周期管理](./lifecycle.md) +- [API 参考文档](./api.md) +- [示例代码](./examples.md) +- [mine.json 配置](./mineJson.md) +- [ConfigProvider 说明](./configProvider.md) \ No newline at end of file diff --git a/docs/zh/plugin/examples.md b/docs/zh/plugin/examples.md new file mode 100644 index 0000000..86d2e00 --- /dev/null +++ b/docs/zh/plugin/examples.md @@ -0,0 +1,1396 @@ +# 插件示例代码 + +本文档提供完整的 MineAdmin 插件开发示例,包括不同类型插件的实际代码案例和最佳实践。 + +## 官方插件示例 + +### App-Store 插件 (混合型) + +**仓库地址**: [mineadmin/appstore](https://github.com/mineadmin/appstore) + +App-Store 是 MineAdmin 唯一的官方默认插件,提供应用市场管理功能,展示了混合型插件的完整实现。 + +#### 核心文件结构 +``` +plugin/mine-admin/app-store/ +├── mine.json # 插件配置 +├── src/ # 后端代码 +│ ├── ConfigProvider.php # 配置提供者 +│ ├── Controller/ # 控制器 +│ ├── Service/ # 服务层 +│ └── Command/ # 命令行 +├── web/ # 前端代码 +│ ├── views/ # 页面组件 +│ └── api/ # API 接口 +└── Database/ # 数据库 +``` + +#### mine.json 配置示例 +```json +{ + "name": "mine-admin/app-store", + "description": "MineAdmin应用市场可视化插件", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "MineAdmin Team", + "role": "developer" + } + ], + "keywords": ["mineadmin", "app-store", "plugin-management"], + "homepage": "https://github.com/mineadmin/appstore", + "license": "MIT", + "composer": { + "require": { + "hyperf/async-queue": "^3.0" + }, + "psr-4": { + "Plugin\\MineAdmin\\AppStore\\": "src" + }, + "config": "Plugin\\MineAdmin\\AppStore\\ConfigProvider" + } +} +``` + +#### ConfigProvider 实现 +```php + [ + // 依赖注入配置 + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'commands' => [ + Command\AppStoreCommand::class, + ], + 'listeners' => [ + Listener\PluginEventListener::class, + ], + 'publish' => [ + [ + 'id' => 'appstore-config', + 'description' => 'App Store configuration file', + 'source' => __DIR__ . '/../publish/appstore.php', + 'destination' => BASE_PATH . '/config/autoload/appstore.php', + ], + ], + ]; + } +} +``` + +## 完整插件开发示例 + +### 1. 用户管理插件 (混合型) + +以下是一个完整的用户管理插件示例,展示如何开发一个包含前后端的混合型插件。 + +#### mine.json 配置 +```json +{ + "name": "mycompany/user-manager", + "description": "用户管理插件", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "composer": { + "require": { + "hyperf/database": "^3.0", + "hyperf/validation": "^3.0" + }, + "psr-4": { + "Plugin\\MyCompany\\UserManager\\": "src" + }, + "config": "Plugin\\MyCompany\\UserManager\\ConfigProvider" + }, + "package": { + "dependencies": { + "element-plus": "^2.4.0" + } + } +} +``` + +#### 核心控制器实现 +```php +request->all(); + $result = $this->service->getPageList($params); + return $this->success($result); + } + + #[PostMapping('/user')] + public function create(): array + { + $data = $this->request->all(); + $user = $this->service->create($data); + return $this->success($user, '用户创建成功'); + } + + #[PutMapping('/user/{id}')] + public function update(int $id): array + { + $data = $this->request->all(); + $this->service->update($id, $data); + return $this->success(null, '更新成功'); + } + + #[DeleteMapping('/user/{id}')] + public function delete(int $id): array + { + $this->service->delete($id); + return $this->success(null, '删除成功'); + } +} +``` + +#### 服务层实现 +```php +where(function($q) use ($params) { + $q->where('username', 'like', "%{$params['keyword']}%") + ->orWhere('email', 'like', "%{$params['keyword']}%"); + }); + } + + $paginator = $query->paginate( + $params['pageSize'] ?? 15, + ['*'], + 'page', + $params['page'] ?? 1 + ); + + return [ + 'items' => $paginator->items(), + 'pageInfo' => [ + 'total' => $paginator->total(), + 'currentPage' => $paginator->currentPage(), + 'totalPage' => $paginator->lastPage() + ] + ]; + } + + public function create(array $data): User + { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + return User::create($data); + } + + public function update(int $id, array $data): bool + { + if (isset($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } + + return User::query()->where('id', $id)->update($data) > 0; + } + + public function delete(int $id): bool + { + return User::destroy($id) > 0; + } +} +``` + +### 2. 后端型插件示例 - API 服务插件 + +以下是一个纯后端 API 服务插件的示例。 + +#### 插件配置 (mine.json) + +```json +{ + "name": "mycompany/api-service", + "description": "API 服务插件", + "version": "1.0.0", + "type": "backend", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "keywords": ["api", "service"], + "license": "MIT", + "composer": { + "require": { + "guzzlehttp/guzzle": "^7.0" + }, + "psr-4": { + "Plugin\\MyCompany\\ApiService\\": "src" + }, + "config": "Plugin\\MyCompany\\ApiService\\ConfigProvider" + } +} +``` + +#### ConfigProvider 实现 + +```php + [ + Contract\ApiClientInterface::class => Service\ApiClient::class, + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'commands' => [ + Command\ApiSyncCommand::class, + ], + 'publish' => [ + [ + 'id' => 'api-service-config', + 'description' => 'API 服务配置文件', + 'source' => __DIR__ . '/../publish/api_service.php', + 'destination' => BASE_PATH . '/config/autoload/api_service.php', + ], + ], + ]; + } +} +``` + +### 3. 前端型插件示例 - 数据可视化插件 + +以下是一个纯前端的数据可视化插件示例。 + +#### mine.json 配置 +```json +{ + "name": "mycompany/data-visualization", + "description": "数据可视化插件", + "version": "1.0.0", + "type": "frontend", + "author": [ + { + "name": "Your Name", + "email": "email@example.com" + } + ], + "package": { + "dependencies": { + "echarts": "^5.4.0", + "vue-echarts": "^6.5.0" + } + } +} +``` + +## 完整插件开发最佳实践 + +### 1. 目录结构规范 + +``` +plugin/vendor-name/plugin-name/ +├── mine.json # 插件配置文件 +├── src/ # PHP 后端代码 +│ ├── ConfigProvider.php # 配置提供者 +│ ├── Controller/ # 控制器 +│ ├── Service/ # 服务层 +│ ├── Model/ # 模型 +│ ├── Command/ # 命令行 +│ ├── Listener/ # 事件监听器 +│ └── Middleware/ # 中间件 +├── web/ # 前端代码 +│ ├── views/ # Vue 页面组件 +│ ├── api/ # API 接口封装 +│ ├── components/ # 公共组件 +│ └── locales/ # 国际化 +├── Database/ # 数据库 +│ ├── Migrations/ # 迁移文件 +│ └── Seeders/ # 数据填充 +└── publish/ # 发布文件 + └── config.php # 配置文件 +``` + +### 2. 命名规范 + +- **插件名称**: 使用 `vendor/plugin-name` 格式 +- **命名空间**: `Plugin\VendorName\PluginName` +- **类名**: 使用大驼峰命名法 +- **方法名**: 使用小驼峰命名法 + +### 3. 核心组件示例 + +#### 控制器示例 + +```php +request->all(); + $result = $this->service->getList($params); + return $this->success($result); + } + + #[PostMapping('/create')] + public function create(): array + { + $data = $this->request->all(); + $result = $this->service->create($data); + return $this->success($result, '创建成功'); + } + $user = $this->userService->find($id); + + if (!$user) { + return $this->error('用户不存在', 404); + } + + return $this->success($user); + } + + /** + * 更新用户 + */ + #[PutMapping('/users/{id:\d+}')] + public function update(int $id): array + { + $data = $this->request->all(); + + $user = $this->userService->update($id, $data); + + return $this->success($user, '用户更新成功'); + } + + /** + * 删除用户 + */ + #[DeleteMapping('/users/{id:\d+}')] + public function destroy(int $id): array + { + $this->userService->delete($id); + + return $this->success([], '用户删除成功'); + } + + /** + * 批量导入用户 + */ + #[PostMapping('/users/import')] + public function import(): array + { + $file = $this->request->file('file'); + + if (!$file || !$file->isValid()) { + return $this->error('请上传有效的文件'); + } + + $result = $this->userService->importFromFile($file); + + return $this->success($result, '导入完成'); + } + + /** + * 导出用户数据 + */ + #[GetMapping('/users/export')] + public function export(): array + { + $params = $this->request->all(); + $filePath = $this->userService->exportToFile($params); + + return $this->success(['file_path' => $filePath], '导出成功'); + } +} +``` + +#### 4. 服务层 (src/Service/UserService.php) + +```php +repository->getList($params); + } + + /** + * 创建用户 + */ + public function create(array $data): array + { + // 密码加密 + if (isset($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } + + // 生成用户头像 + if (!isset($data['avatar'])) { + $data['avatar'] = $this->generateAvatar($data['username']); + } + + $user = $this->repository->create($data); + + // 触发用户创建事件 + event(new UserCreatedEvent($user)); + + return $user->toArray(); + } + + /** + * 更新用户 + */ + public function update(int $id, array $data): array + { + // 密码更新处理 + if (isset($data['password']) && !empty($data['password'])) { + $data['password'] = password_hash($data['password'], PASSWORD_DEFAULT); + } else { + unset($data['password']); + } + + $user = $this->repository->update($id, $data); + + // 触发用户更新事件 + event(new UserUpdatedEvent($user)); + + return $user->toArray(); + } + + /** + * 从文件导入用户 + */ + public function importFromFile($file): array + { + $filePath = $file->getPath() . '/' . $file->getFilename(); + + // 读取 Excel 文件 + $data = $this->parseExcelFile($filePath); + + $successCount = 0; + $errorCount = 0; + $errors = []; + + foreach ($data as $index => $row) { + try { + $this->create([ + 'username' => $row['username'], + 'email' => $row['email'], + 'phone' => $row['phone'] ?? null, + 'password' => $row['password'] ?? '123456', + ]); + $successCount++; + } catch (\Exception $e) { + $errorCount++; + $errors[] = "第{$index}行: " . $e->getMessage(); + } + } + + return [ + 'success_count' => $successCount, + 'error_count' => $errorCount, + 'errors' => $errors + ]; + } + + /** + * 导出用户到文件 + */ + public function exportToFile(array $params = []): string + { + $users = $this->repository->getAllForExport($params); + + // 生成 Excel 文件 + $filePath = $this->generateExcelFile($users); + + return $filePath; + } + + /** + * 生成用户头像 + */ + private function generateAvatar(string $username): string + { + // 使用第三方库生成头像 + $avatar = new \Intervention\Image\ImageManager(); + // ... 头像生成逻辑 + + return '/uploads/avatars/' . $username . '.png'; + } + + /** + * 解析 Excel 文件 + */ + private function parseExcelFile(string $filePath): array + { + // Excel 解析逻辑 + return []; + } + + /** + * 生成 Excel 文件 + */ + private function generateExcelFile(array $users): string + { + // Excel 生成逻辑 + return '/tmp/users_export_' . date('YmdHis') . '.xlsx'; + } + + protected function getRepository(): string + { + return UserRepository::class; + } +} +``` + +#### 5. 数据仓库 (src/Repository/UserRepository.php) + +```php +getModel()::query(); + + // 关键词搜索 + if (!empty($params['keyword'])) { + $query->where(function ($q) use ($params) { + $q->where('username', 'like', "%{$params['keyword']}%") + ->orWhere('email', 'like', "%{$params['keyword']}%") + ->orWhere('phone', 'like', "%{$params['keyword']}%"); + }); + } + + // 状态筛选 + if (isset($params['status'])) { + $query->where('status', $params['status']); + } + + // 角色筛选 + if (!empty($params['role_id'])) { + $query->whereHas('roles', function ($q) use ($params) { + $q->where('id', $params['role_id']); + }); + } + + // 时间范围筛选 + if (!empty($params['created_at'])) { + $dates = explode(' - ', $params['created_at']); + if (count($dates) === 2) { + $query->whereBetween('created_at', [ + $dates[0] . ' 00:00:00', + $dates[1] . ' 23:59:59' + ]); + } + } + + // 排序 + $query->orderBy($params['sort'] ?? 'id', $params['order'] ?? 'desc'); + + return $query->paginate($params['pageSize'] ?? 15)->toArray(); + } + + /** + * 获取导出数据 + */ + public function getAllForExport(array $params = []): array + { + $query = $this->getModel()::query(); + + // 应用相同的筛选条件 + // ... 筛选逻辑 + + return $query->select([ + 'id', 'username', 'email', 'phone', + 'status', 'created_at', 'updated_at' + ])->get()->toArray(); + } +} +``` + +#### 6. 模型 (src/Model/User.php) + +```php + 'integer', + 'last_login_at' => 'datetime:Y-m-d H:i:s', + 'created_at' => 'datetime:Y-m-d H:i:s', + 'updated_at' => 'datetime:Y-m-d H:i:s', + ]; + + /** + * 状态常量 + */ + const STATUS_DISABLED = 0; + const STATUS_ENABLED = 1; + + /** + * 关联角色 + */ + public function roles() + { + return $this->belongsToMany(Role::class, 'user_roles'); + } + + /** + * 获取状态文本 + */ + public function getStatusTextAttribute(): string + { + return match($this->status) { + self::STATUS_ENABLED => '启用', + self::STATUS_DISABLED => '禁用', + default => '未知' + }; + } + + /** + * 获取头像 URL + */ + public function getAvatarUrlAttribute(): string + { + if (empty($this->avatar)) { + return '/default-avatar.png'; + } + + return str_starts_with($this->avatar, 'http') + ? $this->avatar + : config('app.url') . $this->avatar; + } +} +``` + +#### 7. 前端页面 (web/views/UserList.vue) + +```vue + + + + + +``` + +#### 8. API 接口 (web/api/user.js) + +```javascript +// web/api/user.js +import { request } from '@/utils/request' + +const API_BASE = '/user-manager' + +export default { + // 获取用户列表 + getList(params) { + return request({ + url: `${API_BASE}/users`, + method: 'get', + params + }) + }, + + // 创建用户 + create(data) { + return request({ + url: `${API_BASE}/users`, + method: 'post', + data + }) + }, + + // 获取用户详情 + get(id) { + return request({ + url: `${API_BASE}/users/${id}`, + method: 'get' + }) + }, + + // 更新用户 + update(id, data) { + return request({ + url: `${API_BASE}/users/${id}`, + method: 'put', + data + }) + }, + + // 删除用户 + delete(id) { + return request({ + url: `${API_BASE}/users/${id}`, + method: 'delete' + }) + }, + + // 批量导入 + import(file) { + const formData = new FormData() + formData.append('file', file) + + return request({ + url: `${API_BASE}/users/import`, + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + // 导出数据 + export(params) { + return request({ + url: `${API_BASE}/users/export`, + method: 'get', + params + }) + } +} +``` + +#### 9. 数据库迁移 (Database/Migrations/create_users_table.php) + +```php +id(); + $table->string('username', 50)->unique()->comment('用户名'); + $table->string('email')->unique()->comment('邮箱'); + $table->string('phone', 20)->nullable()->comment('手机号'); + $table->string('password')->comment('密码'); + $table->string('avatar')->nullable()->comment('头像'); + $table->tinyInteger('status')->default(1)->comment('状态:0禁用,1启用'); + $table->timestamp('last_login_at')->nullable()->comment('最后登录时间'); + $table->timestamps(); + + $table->index(['username']); + $table->index(['email']); + $table->index(['phone']); + $table->index(['status']); + $table->index(['created_at']); + + $table->comment('用户管理插件-用户表'); + }); + } + + public function down(): void + { + Schema::dropIfExists('plugin_user_manager_users'); + } +} +``` + +#### 10. 安装脚本 (src/InstallScript.php) + +```php +runMigrations(); + + // 2. 初始化数据 + $this->seedData(); + + // 3. 创建必要目录 + $this->createDirectories(); + + // 4. 初始化配置 + $this->initConfig(); + + echo "用户管理插件安装成功!\n"; + return true; + } catch (\Exception $e) { + echo "安装失败: " . $e->getMessage() . "\n"; + return false; + } + } + + private function runMigrations(): void + { + $migrationPath = __DIR__ . '/../Database/Migrations'; + + if (!is_dir($migrationPath)) { + return; + } + + $files = glob($migrationPath . '/*.php'); + sort($files); + + foreach ($files as $file) { + require_once $file; + + $className = $this->getMigrationClassName($file); + $migration = new $className(); + + if (method_exists($migration, 'up')) { + $migration->up(); + echo "执行迁移: " . basename($file) . "\n"; + } + } + } + + private function seedData(): void + { + // 插入默认管理员用户 + Db::table('plugin_user_manager_users')->insertOrIgnore([ + 'username' => 'admin', + 'email' => 'admin@example.com', + 'password' => password_hash('123456', PASSWORD_DEFAULT), + 'status' => 1, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + + echo "初始化默认数据完成\n"; + } + + private function createDirectories(): void + { + $directories = [ + BASE_PATH . '/public/uploads/avatars', + BASE_PATH . '/storage/user-manager', + ]; + + foreach ($directories as $dir) { + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + echo "创建目录: {$dir}\n"; + } + } + } + + private function initConfig(): void + { + $configPath = BASE_PATH . '/config/autoload/user_manager.php'; + + if (!file_exists($configPath)) { + $defaultConfig = [ + 'avatar_upload_path' => '/uploads/avatars', + 'default_password' => '123456', + 'password_reset_expire' => 3600, + 'max_login_attempts' => 5, + ]; + + file_put_contents($configPath, " [ + // 依赖注入配置 + ], + 'annotations' => [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'publish' => [ + // 配置文件发布设置 + ], + ]; + } +} +``` + +### 3. 添加业务逻辑 + +创建控制器 `src/Controller/HelloController.php`: + +```php + 200, + 'message' => 'Hello from MineAdmin Plugin!', + 'data' => [ + 'plugin' => 'hello-world', + 'timestamp' => time() + ] + ]; + } +} +``` + +### 4. 前端开发 (可选) + +在 `web/` 目录下添加前端组件: + +```vue + + + + +``` + +## 安装和测试插件 + +### 1. 安装插件 + +```bash +# 安装插件到系统 +php bin/hyperf.php mine-extension:install mycompany/hello-world --yes +``` + +### 2. 测试功能 + +启动开发服务器并测试 API: + +```bash +# 启动服务 +php bin/hyperf.php start + +# 测试 API (新终端) +curl http://localhost:9501/hello-world/greeting +``` + +### 3. 检查安装状态 + +```bash +# 查看本地已安装插件 +php bin/hyperf.php mine-extension:local-list +``` + +## 插件管理命令 + +### 常用命令总览 + +```bash +# 查看远程插件列表 +php bin/hyperf.php mine-extension:list + +# 下载远程插件 +php bin/hyperf.php mine-extension:download --name plugin-name + +# 安装本地插件 +php bin/hyperf.php mine-extension:install plugin/path --yes + +# 卸载插件 +php bin/hyperf.php mine-extension:uninstall plugin/path --yes + +# 查看本地插件 +php bin/hyperf.php mine-extension:local-list +``` + +## 开发调试技巧 + +### 1. 日志调试 + +在插件中使用 Hyperf 日志系统: + +```php +use Hyperf\Logger\LoggerFactory; + +$logger = $container->get(LoggerFactory::class)->get('plugin'); +$logger->info('Hello World Plugin Debug', ['data' => $someData]); +``` + +### 2. 配置热重载 + +开发期间修改配置后需要重启服务: + +```bash +# 重启 Hyperf 服务 +php bin/hyperf.php start +``` + +### 3. 前端热更新 + +如果使用 MineAdmin 前端开发环境: + +```bash +# 在前端项目目录 +npm run dev +``` + +## 下一步 + +现在您已经创建了第一个插件!接下来可以: + +1. [深入了解插件结构](./structure.md) +2. [学习完整开发流程](./develop.md) +3. [了解生命周期管理](./lifecycle.md) +4. [查看更多示例](./examples.md) + +## 常见问题 + +### Q: 插件安装失败怎么办? +A: 检查 `mine.json` 配置是否正确,确保 PSR-4 自动加载路径正确。 + +### Q: 如何调试插件? +A: 使用 Hyperf 的日志系统和调试工具,查看 `runtime/logs/` 目录下的日志文件。 + +### Q: 前端组件不显示? +A: 确保前端文件放在 `web/` 目录下,安装插件时会自动复制到前端项目。 \ No newline at end of file diff --git a/docs/zh/plugin/index.md b/docs/zh/plugin/index.md index f346bb4..60a192a 100644 --- a/docs/zh/plugin/index.md +++ b/docs/zh/plugin/index.md @@ -1,49 +1,117 @@ -# 准备工作 +# MineAdmin 插件系统 -::: tip -如果开发 MineAdmin 应用;首先,得熟悉 MineAdmin 和 Hyperf 框架,然后做以下的准备工作。 -::: +MineAdmin 插件系统提供了强大的扩展能力,允许开发者创建可复用的功能模块,实现系统的模块化和可扩展性。 -## 获取AccessToken +## 插件系统架构 -MineAdmin不管下载插件应用、更新插件应用还是开发插件应用都需要 `ACCESS_TOKEN` +MineAdmin 的插件系统基于 Hyperf 框架的 ConfigProvider 机制,提供了完整的插件生命周期管理和自动化部署能力。 -获取步骤: +```plantuml +@startuml +!define RECTANGLE class -- 登录 [MineAdmin](https://www.mineadmin.com/login) 官网 -- 进入 `个人中心` 的 [_设置_](https://www.mineadmin.com/member/setting) 页面 -- 点击查看`我的AccessToken` +RECTANGLE "MineAdmin Core" as core { + + bin/hyperf.php + + Plugin::init() +} -::: danger +RECTANGLE "Plugin Management" as mgmt { + + App-Store Component + + Extension Commands + + Plugin Loader +} ---- - -注意 +RECTANGLE "Plugin Structure" as structure { + + mine.json (配置文件) + + src/ (后端代码) + + web/ (前端代码) + + Database/ (数据库) +} -请保管好自己的 AccessToken,不要泄露!!! +RECTANGLE "Official Plugins" as official { + + app-store +} ---- +core --> mgmt : 插件初始化 +mgmt --> structure : 加载插件 +mgmt --> official : 管理官方插件 +structure --> core : 注册服务 -::: +@enduml +``` + +## 核心组件 + +### 1. 插件加载器 +- **文件**: `bin/hyperf.php` ([GitHub](https://github.com/mineadmin/mineadmin/blob/master/bin/hyperf.php)) +- **原理**: 通过 `Plugin::init()` 方法在应用启动时自动加载所有已安装的插件 +- **实现**: 扫描 `plugin/` 目录下的所有插件并注册其 ConfigProvider + +### 2. App-Store 组件 +- **仓库**: [mineadmin/appstore](https://github.com/mineadmin/appstore) +- **功能**: 提供插件的下载、安装、卸载、更新等管理功能 +- **配置**: 通过 `ConfigProvider` 注册服务和配置 + +### 3. 插件配置系统 +- **核心文件**: `mine.json` +- **原理**: 定义插件的元数据、依赖关系、安装脚本等信息 +- **加载**: 在插件安装时解析并注册到系统中 + +## 官方插件 + +MineAdmin 默认提供以下官方插件: -## 配置后端 .env 文件 +| 插件名称 | 功能描述 | 仓库地址 | +|---------|----------|----------| +| app-store | 应用市场管理插件,提供插件的下载、安装、卸载、更新等管理功能 | [GitHub](https://github.com/mineadmin/appstore) | -打开后端根目录的 _.env_ 文件,找到 **MINE_ACCESS_TOKEN** 项,把刚复制的字符串粘贴到 **等号** 后面 +> 注:其他插件如代码生成器、定时任务管理等可通过应用市场获取或自行开发 -```ini [.env] -APP_NAME = MineAdmin +## 插件类型 -APP_ENV = dev +MineAdmin 支持三种类型的插件: -# 省略... +### Mixed (混合型插件) +包含前端和后端完整功能的插件,提供完整的业务模块。 -MINE_ACCESS_TOKEN = 107299501236086 +### Backend (后端插件) +仅包含后端逻辑的插件,主要提供 API 服务和业务逻辑。 + +### Frontend (前端插件) +仅包含前端界面的插件,主要提供用户界面组件。 + +## 快速开始 + +### 环境准备 + +开发 MineAdmin 插件需要: + +1. **熟悉技术栈**:MineAdmin 和 Hyperf 框架 +2. **获取 AccessToken**: + - 登录 [MineAdmin 官网](https://www.mineadmin.com/login) + - 进入个人中心 → [设置页面](https://www.mineadmin.com/member/setting) + - 获取 AccessToken + +3. **配置环境变量**: +```ini +# .env 文件 +MINE_ACCESS_TOKEN=你的AccessToken ``` -## 申请开发者 +::: warning 注意 +请妥善保管 AccessToken,避免泄露! +::: + +### 开发者认证 -如果仅是本地开发应用并且自己使用,这种情况不需要有开发者认证权限,你也可以分发给其他任何人。 +- **本地开发**:无需认证,可自由开发和分发 +- **应用市场发布**:需要开发者认证,联系 MineAdmin 团队开通权限 -如果打算上架官方应用市场,需要进行开发者认证后,才可发布你的应用,并且受到官方版权保护。 +## 相关文档 -目前还未支持线上直接申请认证,需要通过联系 **MineAdmin团队成员** 给你开通开发者权限 \ No newline at end of file +- [快速入门指南](./guide.md) - 创建第一个插件 +- [开发指南](./develop.md) - 详细开发流程 +- [插件结构](./structure.md) - 目录结构规范 +- [生命周期管理](./lifecycle.md) - 安装卸载流程 +- [API 参考](./api.md) - 接口文档 +- [示例代码](./examples.md) - 实际案例 \ No newline at end of file diff --git a/docs/zh/plugin/lifecycle.md b/docs/zh/plugin/lifecycle.md new file mode 100644 index 0000000..6b1b3a9 --- /dev/null +++ b/docs/zh/plugin/lifecycle.md @@ -0,0 +1,743 @@ +# 插件生命周期管理 + +详细介绍 MineAdmin 插件的生命周期管理,包括安装、启用、禁用、更新和卸载的完整流程。 + +## 生命周期概览 + +MineAdmin 插件的生命周期包括以下几个阶段: + +```plantuml +@startuml +!define RECTANGLE class + +state "未安装" as uninstalled +state "已下载" as downloaded +state "已安装" as installed +state "已启用" as enabled +state "已禁用" as disabled +state "需要更新" as needUpdate +state "已卸载" as uninstalled2 + +[*] --> uninstalled +uninstalled --> downloaded : mine-extension:download +downloaded --> installed : mine-extension:install +installed --> enabled : 自动启用 +enabled --> disabled : 禁用插件 +disabled --> enabled : 启用插件 +enabled --> needUpdate : 检测到新版本 +needUpdate --> enabled : mine-extension:update +enabled --> uninstalled2 : mine-extension:uninstall +disabled --> uninstalled2 : mine-extension:uninstall +uninstalled2 --> [*] + +note right of installed : 执行 InstallScript +note right of uninstalled2 : 执行 UninstallScript + +@enduml +``` + +## 插件发现与加载 + +### 1. 插件发现机制 + +**核心实现**: `Plugin::init()` 方法在 `bin/hyperf.php` ([GitHub](https://github.com/mineadmin/mineadmin/blob/master/bin/hyperf.php)) 中调用 + +```plantuml +@startuml +participant "Application" as app +participant "Plugin::init()" as plugin +participant "ConfigProvider" as config +participant "Hyperf Container" as container + +app -> plugin : 应用启动时调用 +plugin -> plugin : 扫描 plugin/ 目录 +plugin -> plugin : 读取 mine.json 配置 +plugin -> config : 加载 ConfigProvider +config -> container : 注册服务到容器 +container -> app : 服务可用 + +note right of plugin : 只加载已安装的插件\n(存在 install.lock 文件) + +@enduml +``` + +### 2. 加载过程详解 + +1. **扫描插件目录**: 遍历 `plugin/` 目录下的所有子目录 +2. **检查安装状态**: 验证是否存在 `install.lock` 文件 +3. **读取配置**: 解析 `mine.json` 配置文件 +4. **加载 ConfigProvider**: 注册插件服务到 Hyperf 容器 +5. **注册路由**: 自动注册控制器路由 +6. **加载中间件**: 注册插件中间件 +7. **注册事件监听器**: 加载事件监听器 + +## 下载阶段 + +### 命令使用 + +```bash +# 下载指定插件 +php bin/hyperf.php mine-extension:download --name plugin-name + +# 查看可下载的插件列表 +php bin/hyperf.php mine-extension:list +``` + +### 下载过程 + +1. **验证 AccessToken**: 检查 `MINE_ACCESS_TOKEN` 环境变量 +2. **请求远程仓库**: 从 MineAdmin 官方仓库获取插件信息 +3. **下载插件包**: 下载压缩包到本地临时目录 +4. **解压文件**: 解压到 `plugin/vendor/plugin-name/` 目录 +5. **验证完整性**: 检查 `mine.json` 文件是否存在且格式正确 + +### 实现原理 + +**核心服务**: App-Store 组件 ([GitHub](https://github.com/mineadmin/appstore)) 提供下载功能 + +```php +// 伪代码示例 +class DownloadService +{ + public function download(string $pluginName): bool + { + // 1. 验证访问令牌 + $this->validateAccessToken(); + + // 2. 获取插件信息 + $pluginInfo = $this->getPluginInfo($pluginName); + + // 3. 下载插件包 + $packagePath = $this->downloadPackage($pluginInfo['download_url']); + + // 4. 解压到目标目录 + $this->extractPackage($packagePath, $this->getPluginPath($pluginName)); + + return true; + } +} +``` + +## 安装阶段 + +### 命令使用 + +```bash +# 安装插件 +php bin/hyperf.php mine-extension:install vendor/plugin-name --yes + +# 强制重新安装 +php bin/hyperf.php mine-extension:install vendor/plugin-name --force +``` + +### 安装流程详解 + +> ⚠️ **重要提示**: 配置文件发布、环境检测和数据库迁移应在 `InstallScript` 中处理,而不是依赖 ConfigProvider 的 publish 功能。 + +```plantuml +@startuml +start + +:检查插件目录; +if (目录存在?) then (是) + :读取 mine.json 配置; + if (配置有效?) then (是) + :检查依赖关系; + if (依赖满足?) then (是) + :安装 Composer 依赖; + :复制前端文件; + #pink:执行 InstallScript; + note right + InstallScript 中处理: + - 环境检测 + - 配置文件发布 + - 数据库迁移 + - 初始数据填充 + end note + if (InstallScript 成功?) then (是) + :创建 install.lock; + :注册到插件列表; + :触发安装事件; + :清理缓存; + stop + else (失败) + :回滚操作; + :清理临时文件; + stop + endif + else (不满足) + :提示安装依赖插件; + stop + endif + else (无效) + :报告配置错误; + stop + endif +else (否) + :报告插件不存在; + stop +endif + +@enduml +``` + +### 1. 前置检查 + +```php +// 安装前检查逻辑 +class InstallChecker +{ + public function check(string $pluginPath): array + { + $errors = []; + + // 检查插件目录 + if (!is_dir($pluginPath)) { + $errors[] = '插件目录不存在'; + } + + // 检查 mine.json + $configPath = $pluginPath . '/mine.json'; + if (!file_exists($configPath)) { + $errors[] = 'mine.json 配置文件不存在'; + } + + // 检查依赖关系 + $config = json_decode(file_get_contents($configPath), true); + foreach ($config['require'] ?? [] as $dependency => $version) { + if (!$this->isDependencyMet($dependency, $version)) { + $errors[] = "依赖 {$dependency} 版本 {$version} 不满足"; + } + } + + return $errors; + } +} +``` + +### 2. Composer 依赖安装 + +安装过程会处理插件的 Composer 依赖: + +```json +// mine.json 中的 composer 配置 +{ + "composer": { + "require": { + "hyperf/async-queue": "^3.0", + "symfony/console": "^6.0" + }, + "psr-4": { + "Plugin\\Vendor\\PluginName\\": "src" + } + } +} +``` + +系统会自动执行: +```bash +composer require hyperf/async-queue:^3.0 symfony/console:^6.0 +``` + +### 3. InstallScript 处理 ⭐ + +> **最佳实践**: 数据库迁移、配置发布和环境检测应在 `InstallScript` 中处理: + +```php +// 在 InstallScript 中处理所有安装逻辑 +class InstallScript +{ + public function handle(): bool + { + // 1. 环境检测 + if (!$this->checkEnvironment()) { + echo "环境不满足要求\n"; + return false; + } + + // 2. 发布配置文件(不使用 ConfigProvider 的 publish) + $this->publishConfig(); + + // 3. 执行数据库迁移 + if (!$this->runMigrations()) { + echo "数据库迁移失败\n"; + return false; + } + + // 4. 初始化数据 + $this->seedData(); + + return true; + } + + private function publishConfig(): void + { + $source = __DIR__ . '/../publish/config/plugin.php'; + $target = BASE_PATH . '/config/autoload/plugin.php'; + + if (!file_exists($target)) { + copy($source, $target); + echo "配置文件已发布\n"; + } + } + + private function runMigrations(): bool + { + $migrationPath = __DIR__ . '/../Database/Migrations'; + + if (is_dir($migrationPath)) { + // 使用 Hyperf 的迁移命令 + $container = \Hyperf\Context\ApplicationContext::getContainer(); + $application = $container->get(\Hyperf\Contract\ApplicationInterface::class); + + $input = new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'migrate', + '--path' => $migrationPath, + ]); + + $output = new \Symfony\Component\Console\Output\BufferedOutput(); + $exitCode = $application->run($input, $output); + + return $exitCode === 0; + } + + return true; + } +} +``` + +### 4. 前端文件复制 + +将 `web/` 目录下的文件复制到前端项目: + +``` +plugin/vendor/plugin-name/web/ → 前端项目对应目录 +├── views/example.vue → src/views/plugin/vendor/plugin-name/example.vue +├── components/ExampleComp.vue → src/components/plugin/vendor/plugin-name/ExampleComp.vue +└── api/example.js → src/api/plugin/vendor/plugin-name/example.js +``` + +### 5. 配置文件发布 ⚠️ + +> **注意**: ConfigProvider 中的 `publish` 功能在插件系统中不可靠,应在 InstallScript 中手动处理: + +```php +// 不推荐:ConfigProvider 中的 publish 可能不生效 +'publish' => [ + // 这种方式在插件中可能不会执行 +] + +// 推荐:在 InstallScript 中手动发布 +protected function publishConfig(): void +{ + $configs = [ + [ + 'source' => __DIR__ . '/../publish/config/plugin.php', + 'target' => BASE_PATH . '/config/autoload/plugin.php', + ], + [ + 'source' => __DIR__ . '/../publish/config/routes.php', + 'target' => BASE_PATH . '/config/routes/plugin.php', + ], + ]; + + foreach ($configs as $config) { + if (!file_exists($config['target'])) { + copy($config['source'], $config['target']); + echo "配置文件已发布: {$config['target']}\n"; + } + } +} +``` + +### 6. 创建安装锁文件 + +安装成功后创建 `install.lock` 文件标记安装状态: + +``` +plugin/vendor/plugin-name/install.lock +``` + +文件内容包含安装信息: +```json +{ + "installed_at": "2024-01-01 12:00:00", + "version": "1.0.0", + "installer": "admin", + "checksum": "abc123..." +} +``` + +## 启用/禁用管理 + +### 插件状态控制 + +MineAdmin 支持在不卸载插件的情况下临时禁用插件: + +```bash +# 禁用插件 +php bin/hyperf.php mine-extension:disable vendor/plugin-name + +# 启用插件 +php bin/hyperf.php mine-extension:enable vendor/plugin-name + +# 查看插件状态 +php bin/hyperf.php mine-extension:status vendor/plugin-name +``` + +### 状态管理机制 + +状态信息存储在 `install.lock` 文件中: + +```json +{ + "installed_at": "2024-01-01 12:00:00", + "version": "1.0.0", + "status": "enabled", // enabled | disabled + "disabled_at": null, + "disabled_reason": null +} +``` + +## 更新阶段 + +### 更新检查 + +```bash +# 检查插件更新 +php bin/hyperf.php mine-extension:check-updates + +# 更新指定插件 +php bin/hyperf.php mine-extension:update vendor/plugin-name + +# 更新所有插件 +php bin/hyperf.php mine-extension:update-all +``` + +### 更新流程 + +```plantuml +@startuml +start + +:检查远程版本; +if (有新版本?) then (是) + :备份当前插件; + :下载新版本; + :验证完整性; + :执行更新前脚本; + :替换插件文件; + :执行数据库迁移; + :更新配置文件; + :执行更新后脚本; + if (更新成功?) then (是) + :更新版本信息; + :清理备份; + :触发更新事件; + stop + else (失败) + :恢复备份; + :报告错误; + stop + endif +else (否) + :无需更新; + stop +endif + +@enduml +``` + +### 版本兼容性处理 + +更新时会检查版本兼容性: + +```php +class UpdateManager +{ + public function checkCompatibility(string $currentVersion, string $newVersion): bool + { + // 检查主版本兼容性 + $current = $this->parseVersion($currentVersion); + $new = $this->parseVersion($newVersion); + + // 主版本不同时可能存在破坏性更新 + if ($current['major'] !== $new['major']) { + return $this->checkBreakingChanges($currentVersion, $newVersion); + } + + return true; + } +} +``` + +## 卸载阶段 + +### 命令使用 + +```bash +# 卸载插件 +php bin/hyperf.php mine-extension:uninstall vendor/plugin-name --yes + +# 强制卸载 (忽略错误) +php bin/hyperf.php mine-extension:uninstall vendor/plugin-name --force +``` + +### 卸载流程 + +```plantuml +@startuml +start + +:检查插件状态; +if (插件已安装?) then (是) + :检查依赖关系; + if (有其他插件依赖?) then (是) + :提示依赖冲突; + if (强制卸载?) then (是) + :继续卸载; + else (否) + :取消卸载; + stop + endif + endif + + :执行 UninstallScript; + :删除数据库表; + :清理配置文件; + :删除前端文件; + :清理缓存; + :移除 Composer 依赖; + :删除插件目录; + :清理注册信息; + :触发卸载事件; + stop +else (否) + :插件未安装; + stop +endif + +@enduml +``` + +### 卸载脚本执行 + +```php +// UninstallScript 示例 +class UninstallScript +{ + public function handle(): bool + { + try { + // 1. 清理数据库 + $this->cleanDatabase(); + + // 2. 清理配置文件 + $this->cleanConfigFiles(); + + // 3. 清理缓存数据 + $this->cleanCache(); + + // 4. 清理日志文件 + $this->cleanLogs(); + + // 5. 执行自定义清理逻辑 + $this->customCleanup(); + + return true; + } catch (\Exception $e) { + logger()->error('插件卸载失败: ' . $e->getMessage()); + return false; + } + } + + private function cleanDatabase(): void + { + // 删除插件相关表 + DB::statement('DROP TABLE IF EXISTS plugin_example'); + + // 清理配置数据 + DB::table('system_config')->where('key', 'like', 'plugin.example.%')->delete(); + } +} +``` + +## 错误处理与回滚 + +### 安装错误回滚 + +如果安装过程中出现错误,系统会自动回滚: + +```php +class InstallRollback +{ + public function rollback(string $pluginPath, array $operations): void + { + foreach (array_reverse($operations) as $operation) { + try { + switch ($operation['type']) { + case 'database': + $this->rollbackDatabase($operation['data']); + break; + case 'files': + $this->rollbackFiles($operation['data']); + break; + case 'config': + $this->rollbackConfig($operation['data']); + break; + } + } catch (\Exception $e) { + logger()->error('回滚操作失败: ' . $e->getMessage()); + } + } + } +} +``` + +### 依赖冲突处理 + +当插件之间存在依赖冲突时的处理策略: + +```php +class DependencyResolver +{ + public function resolveConflicts(array $conflicts): array + { + $solutions = []; + + foreach ($conflicts as $conflict) { + $solution = match($conflict['type']) { + 'version_conflict' => $this->resolveVersionConflict($conflict), + 'circular_dependency' => $this->resolveCircularDependency($conflict), + 'missing_dependency' => $this->resolveMissingDependency($conflict), + default => null + }; + + if ($solution) { + $solutions[] = $solution; + } + } + + return $solutions; + } +} +``` + +## 事件系统 + +插件生命周期的各个阶段都会触发相应事件: + +### 事件列表 + +```php +// 插件生命周期事件 +class PluginEvents +{ + const BEFORE_INSTALL = 'plugin.before_install'; + const AFTER_INSTALL = 'plugin.after_install'; + const BEFORE_UNINSTALL = 'plugin.before_uninstall'; + const AFTER_UNINSTALL = 'plugin.after_uninstall'; + const BEFORE_UPDATE = 'plugin.before_update'; + const AFTER_UPDATE = 'plugin.after_update'; + const ENABLED = 'plugin.enabled'; + const DISABLED = 'plugin.disabled'; +} +``` + +### 事件监听器示例 + +```php +use Hyperf\Event\Annotation\Listener; +use Hyperf\Event\Contract\ListenerInterface; + +#[Listener] +class PluginInstallListener implements ListenerInterface +{ + public function listen(): array + { + return [ + PluginEvents::AFTER_INSTALL, + ]; + } + + public function process(object $event): void + { + // 插件安装后的处理逻辑 + logger()->info('插件安装完成', [ + 'plugin' => $event->getPluginName(), + 'version' => $event->getVersion() + ]); + + // 清理缓存 + $this->clearCache($event->getPluginName()); + + // 发送通知 + $this->sendNotification($event); + } +} +``` + +## 状态查询 + +### 查看插件状态 + +```bash +# 查看所有本地插件状态 +php bin/hyperf.php mine-extension:local-list + +# 查看远程可用插件 +php bin/hyperf.php mine-extension:list + +# 查看特定插件详情 +php bin/hyperf.php mine-extension:info vendor/plugin-name +``` + +### 状态信息结构 + +```json +{ + "name": "vendor/plugin-name", + "version": "1.0.0", + "status": "enabled", + "installed_at": "2024-01-01 12:00:00", + "last_updated": "2024-01-15 10:30:00", + "dependencies": [ + "vendor/dependency-plugin" + ], + "dependents": [ + "vendor/dependent-plugin" + ], + "file_integrity": "valid", + "database_status": "migrated" +} +``` + +## 最佳实践 + +### 1. 安装脚本设计 + +- 实现幂等性:多次执行结果一致 +- 提供详细的错误信息 +- 支持事务回滚 +- 记录操作日志 + +### 2. 卸载脚本设计 + +- 完全清理插件数据 +- 保留用户重要数据的备份选项 +- 处理依赖关系 +- 优雅降级 + +### 3. 版本管理 + +- 遵循语义化版本规范 +- 提供升级路径说明 +- 标注破坏性更新 +- 维护更新日志 + +## 相关文档 + +- [插件开发指南](./develop.md) - 开发流程 +- [插件结构说明](./structure.md) - 目录结构 +- [API 参考](./api.md) - 接口文档 +- [示例代码](./examples.md) - 实践案例 \ No newline at end of file diff --git a/docs/zh/plugin/structure.md b/docs/zh/plugin/structure.md index b8f7548..cab85f3 100644 --- a/docs/zh/plugin/structure.md +++ b/docs/zh/plugin/structure.md @@ -1,20 +1,471 @@ # 插件目录结构 -一个标准插件目录结构说明 - ---- - -以[插件创建章节](./create.md)为例 - -```shell -- plugin/test/demo 插件根目录 --- plugin/test/demo/src 插件后端目录 ---- plugin/test/demo/src/InstallScript.php 插件安装时执行类方法 ---- plugin/test/demo/src/UninstallScript.php 插件卸载时执行类方法 ---- plugin/test/demo/src/ConfigProvider.php 插件配置目录,此文件与 hyperf 官方配置一致 --- plugin/test/demo/Database 插件数据库迁移与填充文件目录 ---- plugin/test/demo/Database/Migrations 插件数据库迁移文件 ---- plugin/test/demo/Database/Seeder 插件数据库数据填充文件 --- plugin/test/demo/web 插件前端目录 --- plugin/test/demo/mine.json 插件核心信息文件 -``` \ No newline at end of file +详细介绍 MineAdmin 插件的标准目录结构、文件规范和组织方式。 + +## 标准目录结构 + +一个完整的 MineAdmin 插件目录结构如下: + +``` +plugin/vendor/plugin-name/ # 插件根目录 +├── mine.json # 插件核心配置文件 ⭐ +├── README.md # 插件说明文档 +├── LICENSE # 许可证文件 +├── composer.json # Composer 依赖配置 (可选) +├── src/ # 后端源码目录 ⭐ +│ ├── ConfigProvider.php # 配置提供者 ⭐ +│ ├── InstallScript.php # 安装脚本 ⭐ +│ ├── UninstallScript.php # 卸载脚本 ⭐ +│ ├── Controller/ # 控制器目录 +│ │ ├── AdminController.php # 管理员控制器 +│ │ └── ApiController.php # API 控制器 +│ ├── Service/ # 服务层目录 +│ │ └── ExampleService.php # 业务服务类 +│ ├── Repository/ # 仓库层目录 +│ │ └── ExampleRepository.php # 数据仓库类 +│ ├── Model/ # 模型目录 +│ │ └── Example.php # 数据模型 +│ ├── Request/ # 请求验证目录 +│ │ ├── CreateRequest.php # 创建请求验证 +│ │ └── UpdateRequest.php # 更新请求验证 +│ ├── Resource/ # 资源转换目录 +│ │ └── ExampleResource.php # 资源转换类 +│ ├── Middleware/ # 中间件目录 +│ │ └── ExampleMiddleware.php # 自定义中间件 +│ ├── Command/ # 命令行目录 +│ │ └── ExampleCommand.php # 自定义命令 +│ ├── Listener/ # 事件监听器目录 +│ │ └── ExampleListener.php # 事件监听器 +│ └── Exception/ # 异常处理目录 +│ └── ExampleException.php # 自定义异常 +├── web/ # 前端源码目录 ⭐ +│ ├── views/ # 页面组件目录 +│ │ ├── index.vue # 主页面 +│ │ ├── list.vue # 列表页面 +│ │ └── form.vue # 表单页面 +│ ├── components/ # 公共组件目录 +│ │ └── ExampleComponent.vue # 通用组件 +│ ├── api/ # API 接口目录 +│ │ └── example.js # 接口定义 +│ ├── router/ # 路由配置目录 +│ │ └── index.js # 路由配置 +│ ├── store/ # 状态管理目录 +│ │ └── example.js # 状态管理 +│ └── assets/ # 静态资源目录 +│ ├── images/ # 图片资源 +│ └── styles/ # 样式文件 +├── Database/ # 数据库相关目录 ⭐ +│ ├── Migrations/ # 数据库迁移文件 +│ │ └── 2024_01_01_000000_create_example_table.php +│ └── Seeders/ # 数据填充文件 +│ └── ExampleSeeder.php # 数据填充类 +├── config/ # 配置文件目录 +│ └── example.php # 插件配置文件 +├── publish/ # 发布文件目录 +│ ├── config/ # 配置文件模板 +│ │ └── example.php # 配置文件模板 +│ └── assets/ # 静态资源模板 +├── tests/ # 测试文件目录 +│ ├── Unit/ # 单元测试 +│ ├── Feature/ # 功能测试 +│ └── TestCase.php # 测试基类 +├── docs/ # 文档目录 +│ ├── installation.md # 安装文档 +│ ├── usage.md # 使用文档 +│ └── api.md # API 文档 +└── .gitignore # Git 忽略文件 +``` + +## 核心文件详解 + +### 1. mine.json (插件配置文件) + +**文件路径**: `mine.json` ([配置详解](./mineJson.md)) + +插件的核心配置文件,定义插件的基本信息、依赖关系和加载配置: + +```json +{ + "name": "vendor/plugin-name", + "description": "插件描述", + "version": "1.0.0", + "type": "mixed", + "author": [ + { + "name": "Author Name", + "email": "author@example.com", + "role": "developer" + } + ], + "keywords": ["mineadmin", "plugin"], + "homepage": "https://github.com/vendor/plugin-name", + "license": "MIT", + "require": { + "php": ">=8.1", + "hyperf/framework": "^3.0" + }, + "package": { + "dependencies": { + "vue": "^3.0", + "element-plus": "^2.0" + } + }, + "composer": { + "require": { + "hyperf/async-queue": "^3.0" + }, + "psr-4": { + "Plugin\\Vendor\\PluginName\\": "src" + }, + "config": "Plugin\\Vendor\\PluginName\\ConfigProvider" + } +} +``` + +### 2. ConfigProvider.php (配置提供者) + +**文件路径**: `src/ConfigProvider.php` +**实现原理**: 基于 Hyperf ConfigProvider 机制 ([GitHub](https://github.com/hyperf/hyperf/blob/master/src/config-provider/src/ConfigProvider.php)) + +> ⚠️ **注意**: ConfigProvider 中的 `publish` 功能在插件系统中存在问题,建议在 InstallScript 中处理配置文件发布。 + +```php + [], + 'annotations' => [ + 'scan' => [ + 'paths' => [__DIR__], + ], + ], + 'commands' => [], + 'listeners' => [], + // publish 功能在插件中不推荐使用 + // 请在 InstallScript 中处理配置文件发布 + ]; + } +} +``` + +### 3. InstallScript.php (安装脚本) ⭐ + +**文件路径**: `src/InstallScript.php` +**调用时机**: 执行 `mine-extension:install` 命令时 +**重要性**: 推荐在此处理配置发布、环境检测和数据库迁移 + +```php +checkEnvironment()) { + echo "环境检测失败\n"; + return false; + } + + // 2. 发布配置文件 + $this->publishConfig(); + + // 3. 执行数据库迁移 + $this->runMigrations(); + + // 4. 初始化数据 + $this->seedData(); + + echo "插件安装成功\n"; + return true; + } + + protected function checkEnvironment(): bool + { + // 检查 PHP 版本 + if (version_compare(PHP_VERSION, '8.1.0', '<')) { + echo "PHP 版本需要 >= 8.1\n"; + return false; + } + + // 检查必要的扩展 + $requiredExtensions = ['redis', 'pdo', 'json']; + foreach ($requiredExtensions as $ext) { + if (!extension_loaded($ext)) { + echo "缺少 PHP 扩展: {$ext}\n"; + return false; + } + } + + return true; + } + + protected function publishConfig(): void + { + $source = __DIR__ . '/../publish/config/plugin.php'; + $target = BASE_PATH . '/config/autoload/plugin.php'; + + if (!file_exists($target)) { + copy($source, $target); + echo "配置文件已发布: {$target}\n"; + } + } + + protected function runMigrations(): void + { + $migrationPath = __DIR__ . '/../Database/Migrations'; + + if (is_dir($migrationPath)) { + // 执行迁移命令 + $container = \Hyperf\Context\ApplicationContext::getContainer(); + $application = $container->get(ApplicationInterface::class); + $application->setAutoExit(false); + + $input = new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'migrate', + '--path' => $migrationPath, + ]); + + $output = new \Symfony\Component\Console\Output\BufferedOutput(); + $application->run($input, $output); + + echo "数据库迁移完成\n"; + } + } + + protected function seedData(): void + { + // 初始化默认数据 + // 例如创建默认配置、菜单等 + } +} +``` + +### 4. UninstallScript.php (卸载脚本) ⭐ + +**文件路径**: `src/UninstallScript.php` +**调用时机**: 执行 `mine-extension:uninstall` 命令时 +**重要性**: 清理配置文件、数据表和相关资源 + +```php +backupData(); + + // 2. 删除数据库表 + $this->dropTables(); + + // 3. 清理配置文件 + $this->removeConfig(); + + // 4. 清理缓存 + $this->clearCache(); + + echo "插件卸载完成\n"; + return true; + } + + protected function backupData(): void + { + // 备份重要数据到指定目录 + $backupPath = BASE_PATH . '/runtime/backup/plugin_' . date('YmdHis') . '.sql'; + // 实现备份逻辑 + } + + protected function dropTables(): void + { + // 删除插件创建的数据表 + $tables = ['plugin_example_table', 'plugin_settings']; + + foreach ($tables as $table) { + if (Db::schema()->hasTable($table)) { + Db::schema()->drop($table); + echo "已删除数据表: {$table}\n"; + } + } + } + + protected function removeConfig(): void + { + $configFile = BASE_PATH . '/config/autoload/plugin.php'; + + if (file_exists($configFile)) { + unlink($configFile); + echo "配置文件已删除: {$configFile}\n"; + } + } + + protected function clearCache(): void + { + // 清理插件相关缓存 + $redis = \Hyperf\Context\ApplicationContext::getContainer() + ->get(\Hyperf\Redis\Redis::class); + + $redis->del('plugin:cache:*'); + echo "缓存已清理\n"; + } +} +``` + +## 目录结构图解 + +```plantuml +@startuml +!define FOLDER rectangle +!define FILE rectangle + +FOLDER "Plugin Root" as root { + FILE "mine.json" as config + FOLDER "src/" as src { + FILE "ConfigProvider.php" as provider + FILE "InstallScript.php" as install + FILE "UninstallScript.php" as uninstall + FOLDER "Controller/" as controller + FOLDER "Service/" as service + FOLDER "Model/" as model + } + FOLDER "web/" as web { + FOLDER "views/" as views + FOLDER "components/" as components + FOLDER "api/" as api + } + FOLDER "Database/" as database { + FOLDER "Migrations/" as migrations + FOLDER "Seeders/" as seeders + } +} + +config --> provider : 配置加载 +provider --> install : 安装时调用 +provider --> uninstall : 卸载时调用 +web --> views : 前端页面 +database --> migrations : 数据库结构 +database --> seeders : 初始数据 + +@enduml +``` + +## 不同类型插件的结构差异 + +### Mixed (混合型插件) +包含完整的 `src/` 和 `web/` 目录,提供前后端完整功能。 + +### Backend (后端插件) +只包含 `src/` 目录,专注于提供 API 服务和业务逻辑: + +``` +plugin/vendor/backend-plugin/ +├── mine.json +├── src/ +│ ├── ConfigProvider.php +│ ├── Controller/ +│ ├── Service/ +│ └── Model/ +└── Database/ +``` + +### Frontend (前端插件) +只包含 `web/` 目录,专注于前端界面和交互: + +``` +plugin/vendor/frontend-plugin/ +├── mine.json +├── web/ +│ ├── views/ +│ ├── components/ +│ └── assets/ +└── src/ + └── ConfigProvider.php # 最小配置 +``` + +## 命名规范 + +### 1. 目录命名 +- 使用小写字母和连字符:`user-management` +- 避免使用下划线和空格 + +### 2. 文件命名 +- PHP 类文件使用 PascalCase:`UserController.php` +- Vue 组件使用 PascalCase:`UserList.vue` +- 配置文件使用小写:`user.php` + +### 3. 命名空间规范 +遵循 PSR-4 自动加载标准: + +```php +// 插件路径: plugin/mineadmin/user-manager/ +// 命名空间: Plugin\MineAdmin\UserManager\ +namespace Plugin\MineAdmin\UserManager\Controller; +``` + +## 文件权限和安全 + +### 1. 文件权限设置 +```bash +# 设置合适的文件权限 +find plugin/ -type f -name "*.php" -exec chmod 644 {} \; +find plugin/ -type d -exec chmod 755 {} \; +``` + +### 2. 安全注意事项 +- 敏感配置使用环境变量 +- 避免在代码中硬编码密钥 +- 验证和过滤用户输入 +- 使用 HTTPS 传输敏感数据 + +## 最佳实践 + +### 1. 文件组织 +- 按功能模块组织代码 +- 保持目录结构清晰 +- 使用有意义的文件名 + +### 2. 代码规范 +- 遵循 PSR-12 编码标准 +- 添加适当的注释 +- 使用类型声明 + +### 3. 版本控制 +- 使用 `.gitignore` 排除不必要的文件 +- 创建清晰的提交信息 +- 使用语义化版本号 + +## 示例项目结构 + +查看官方插件的实际结构: + +**App-Store 插件**: MineAdmin 官方应用市场插件,展示了标准的混合型插件结构 + +## 常见问题 + +### Q: 插件目录应该放在哪里? +A: 插件应该放在项目根目录的 `plugin/` 目录下,按 `vendor/plugin-name` 格式组织。 + +### Q: 如何处理插件之间的依赖? +A: 在 `mine.json` 的 `require` 字段中声明依赖的其他插件。 + +### Q: 前端文件安装后放在哪里? +A: `web/` 目录下的文件会在安装时复制到前端项目的对应位置。 + +### Q: 数据库迁移文件如何执行? +A: 在 `InstallScript.php` 中调用迁移执行逻辑,或使用 Hyperf 的迁移命令。 \ No newline at end of file