初始化本地仓库

This commit is contained in:
2026-08-31 11:33:59 +08:00
commit 663f74b633
2390 changed files with 402918 additions and 0 deletions
+449
View File
@@ -0,0 +1,449 @@
<?php
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: http://www.madong.tech
*/
/**
* 插件安装基类
* 使用 Trait 实现职责分离:
* - ConfigTrait: 配置管理
* - MigrationTrait: 迁移操作
* - SeedTrait: 种子操作
* - MenuTrait: 菜单操作
* - TemplateTrait: 模板资源操作
* - DependencyTrait: 依赖安装
* 使用方式:
* 1. 在插件目录创建 Install.php
* 2. 继承此类即可,迁移和种子会自动执行
* 3. 可重写 install/uninstall/update 方法添加自定义逻辑
* 配置覆盖:
* - 默认配置: core/plugin/config/default.php
* - 应用配置: core/plugin/config/app.php
* - 插件配置: plugin/{name}/config/info.php (可覆盖上述配置)
*/
namespace core\plugin;
use core\plugin\traits\ConfigTrait;
use core\plugin\traits\MigrationTrait;
use core\plugin\traits\SeedTrait;
use core\plugin\traits\MenuTrait;
use core\plugin\traits\TemplateTrait;
use core\plugin\traits\DependencyTrait;
class PluginInstall
{
use ConfigTrait;
use MigrationTrait;
use SeedTrait;
use MenuTrait;
use TemplateTrait;
use DependencyTrait;
/** @var string 插件名称 */
protected string $pluginName = '';
/** @var string 插件路径 */
protected string $pluginPath = '';
/** @var string|null 连接名(默认使用框架配置) */
protected ?string $connection = null;
/** @var array 插件配置 */
protected array $appConfig = [];
/** @var callable|null 进度回调函数(用于在线模式通过 SSE 返回进度) */
protected $progressCallback = null;
/** @var bool 是否为在线模式(非 CLI) */
protected bool $isOnlineMode = false;
/** @var bool 卸载阶段是否保留已复制的模板资源 */
protected bool $preserveTemplatesDuringUninstall = false;
/**
* @param callable|null $progressCallback 进度回调
* @param bool $isOnlineMode 是否为在线模式
*/
public function __construct(?callable $progressCallback = null, bool $isOnlineMode = false)
{
$this->progressCallback = $progressCallback;
$this->isOnlineMode = $isOnlineMode;
$this->init();
}
/**
* 设置进度回调
*/
public function setProgressCallback(?callable $callback): self
{
$this->progressCallback = $callback;
return $this;
}
/**
* 设置是否为在线模式
*/
public function setOnlineMode(bool $isOnlineMode): self
{
$this->isOnlineMode = $isOnlineMode;
return $this;
}
/**
* 输出日志(CLI 模式 echo,在线模式通过回调)
*/
protected function log(string $message, ?int $progress = null): void
{
if ($this->isOnlineMode && $this->progressCallback) {
// 在线模式:通过回调返回进度
($this->progressCallback)($message, $progress);
} else {
// CLI 模式:直接输出
echo $message . "\n";
}
}
/**
* 输出进度
*/
protected function progress(string $message, int $progress): void
{
$this->log($message, $progress);
}
/**
* 输出消息(用于 Trait 中的 echo 替换)
* CLI 模式直接输出,在线模式输出到缓冲区
*/
protected function output(string $message): void
{
echo $message . "\n";
}
/**
* 初始化
*/
protected function init(): void
{
$this->pluginPath = $this->getPluginPath();
$this->pluginName = basename($this->pluginPath);
$this->appConfig = $this->getAppConfig();
}
/**
* 插件路径 - 子类可重写
*/
protected function getPluginPath(): string
{
$class = static::class;
$parts = explode('\\', $class);
if (count($parts) >= 2 && $parts[0] === 'plugin') {
return base_path('plugin/' . $parts[1]);
}
return base_path('plugin');
}
/**
* 插件名称
*/
protected function getPluginName(): string
{
return $this->pluginName ?: basename($this->getPluginPath());
}
/**
* 设置数据库连接
*/
public function setConnection(?string $connection): self
{
$this->connection = $connection;
return $this;
}
/**
* 强制重新运行迁移
*/
public function forceMigrate(): void
{
$this->output("⚠️ Force migration - clearing migration logs...");
$this->clearLogs();
$this->runMigrations();
}
/**
* 安装
*/
public function install($version): void
{
$this->log("====== Plugin Install: {$this->getPluginName()} ======", 0);
$this->log("Version: {$version}");
$this->log("Path: {$this->pluginPath}");
// 安装前回调
$this->beforeInstall($version);
// 安装 Composer 依赖(后端)
$this->installComposerDeps();
// 运行迁移(后端)
$this->runMigrations();
// 运行种子(后端)
$this->runSeeds();
// 安装前端依赖
$this->installNpmDeps();
// 复制前端模板到各端
$this->copyTemplates();
// 执行各端安装命令
$this->runInstallCommands();
// 安装后回调 - 可重写用于导入菜单等操作
$this->afterInstall($version);
// 保存插件安装状态
$this->savePluginConfig($version);
$this->log("====== Install Completed ======", 100);
}
/**
* 安装前回调 - 可重写
*/
protected function beforeInstall(string $version): void
{
$this->log(" 🔄 Running beforeInstall...");
}
/**
* 安装后回调 - 可重写
*/
protected function afterInstall(string $version): void
{
$this->log(" 🔄 Running afterInstall...");
// 加载插件菜单
$this->loadMenus();
}
/**
* 卸载前回调 - 可重写
*/
protected function beforeUninstall(string $version): void
{
$this->log(" 🔄 Running beforeUninstall...");
}
/**
* 卸载后回调 - 清理菜单
*/
protected function afterUninstall(string $version): void
{
$this->log(" 🔄 Running afterUninstall...");
// 卸载菜单
$this->uninstallPluginMenus();
}
/**
* 卸载
*/
public function uninstall($version): void
{
$this->log("====== Plugin Uninstall: {$this->getPluginName()} ======", 0);
$this->preserveTemplatesDuringUninstall = true;
try {
// 卸载前回调和卸载后回调均不得物理删除模板资源。
$this->beforeUninstall($version);
// 获取卸载配置
$pluginInfo = $this->getPluginInfo();
$uninstallConfig = $pluginInfo['uninstall'] ?? [];
$dropTables = $uninstallConfig['drop_tables'] ?? false;
$this->log("📋 Uninstall config: drop_tables=" . ($dropTables ? 'true' : 'false'));
// 回滚迁移(可选删除表)
$this->rollbackMigrations($dropTables);
// 清理日志
$this->clearLogs();
$this->afterUninstall($version);
} finally {
$this->preserveTemplatesDuringUninstall = false;
}
$this->log('📦 Plugin package, templates and dependencies are preserved until deletion; only the installation marker is removed.');
$this->log("====== Uninstall Completed ======", 100);
}
/**
* 删除插件
*/
public function delete($version): void
{
$this->log("====== Plugin Delete: {$this->getPluginName()} ======", 10);
// 获取插件配置
$pluginInfo = $this->getPluginInfo();
$uninstallConfig = $pluginInfo['uninstall'] ?? [];
$undeletable = $uninstallConfig['undeletable'] ?? false;
$this->log("📋 Plugin config: undeletable=" . ($undeletable ? 'true' : 'false'), 20);
// 检查是否允许删除
if ($undeletable) {
throw new \Exception('系统内置插件不允许删除');
}
// 删除前回调
$this->beforeDelete($version);
// 删除安装时复制到前端项目的模板资源
$this->deleteTemplates();
// 按插件配置移除安装时合并的依赖
$globalUninstallConfig = $this->getConfig('uninstall', []);
$removeDependencies = $uninstallConfig['remove_dependencies'] ?? $globalUninstallConfig['remove_dependencies'] ?? false;
if ($removeDependencies) {
$this->removeMergedDependencies();
}
// 获取插件目录
$pluginDir = $this->pluginPath;
if (!is_dir($pluginDir)) {
throw new \Exception('插件目录不存在:' . $pluginDir);
}
$this->log("📁 Deleting plugin directory: {$pluginDir}", 40);
// 使用递归删除整个插件目录
$this->removeDirectory($pluginDir);
$this->log("✅ Plugin deleted successfully", 100);
// 删除后回调
$this->afterDelete($version);
}
/**
* 删除前回调 - 可重写
*/
protected function beforeDelete(string $version): void
{
$this->log(" 🔄 Running beforeDelete...");
}
/**
* 删除后回调 - 可重写
*/
protected function afterDelete(string $version): void
{
$this->log(" 🔄 Running afterDelete...");
}
/**
* 递归删除目录
*/
protected function removeDirectory(string $directory): void
{
if (!is_dir($directory)) {
return;
}
$files = scandir($directory);
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$path = $directory . DIRECTORY_SEPARATOR . $file;
if (is_dir($path)) {
$this->removeDirectory($path);
} else {
@unlink($path);
}
}
@rmdir($directory);
}
/**
* 删除模板资源 - 仅应在删除插件或升级清理阶段调用
*/
protected function deletePluginTemplates(): void
{
$this->deleteTemplates();
}
/**
* 更新
*/
public function update($version): void
{
$this->output("====== Plugin Update: {$this->getPluginName()} ======");
// 先卸载
$this->uninstall($version);
// 升级前清理旧模板,避免已删除的旧文件残留
$this->deleteTemplates();
// 再安装
$this->install($version);
$this->output("====== Update Completed ======");
}
// =========================================================
// 端安装命令执行
// =========================================================
/**
* 执行各端安装命令
*/
protected function runInstallCommands(): void
{
$this->output("📦 Running install commands...");
$projectRoot = $this->getProjectRoot();
// 后端安装命令
$this->runBackendCommands($projectRoot);
// 前端安装命令
$this->runFrontendCommands($projectRoot);
}
/**
* 执行后端安装命令
*/
protected function runBackendCommands(string $projectRoot): void
{
// 可在子类中重写实现自定义命令
}
/**
* 执行前端安装命令
*/
protected function runFrontendCommands(string $projectRoot): void
{
// 可在子类中重写实现自定义命令
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
/**
* 插件迁移基类
*
* 迁移文件写法(匿名类方式):
*
* <?php
* use Illuminate\Database\Schema\Builder;
* use Illuminate\Database\Schema\Blueprint;
*
* return new class {
* public function up(Builder $schema): void
* {
* $schema->create('official_app', function (Blueprint $table) {
* $table->bigInteger('id')->primary();
* $table->string('name', 50);
* });
* }
*
* public function down(Builder $schema): void
* {
* $schema->dropIfExists('official_app');
* }
* };
*/
namespace core\plugin;
use Illuminate\Database\Schema\Builder;
use Illuminate\Database\Schema\Blueprint;
abstract class PluginMigrate
{
/**
* 迁移向上执行(安装)
*/
abstract public function up(Builder $schema): void;
/**
* 迁移向下执行(回滚)
*/
public function down(Builder $schema): void
{
// 可选实现
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: http://www.madong.tech
*/
/**
* 插件默认配置
* 此文件的配置可被插件的 config/info.php 覆盖
* 覆盖规则:info.php 中的配置会合并到此文件的配置中
*/
return [
'enable' => true,
/**
* 安装配置
*/
'install' => [
// 是否自动创建日志目录
'auto_mkdir_logs' => true,
],
/**
* 卸载配置
*/
'uninstall' => [
// 卸载时是否删除数据表(默认 false)
'drop_tables' => false,
// 卸载时是否移除合并的依赖(默认 false)
'remove_dependencies' => false,
],
/**
* 模板资源配置
* 插件模板复制目标位置(相对于项目根目录,即 server 的兄弟节点)
* 结构: ['端' => '相对路径']
*/
'template' => [
// 后台模板目标路径(相对于项目根目录)
'admin' => 'frontend/admin/src/apps',
// 前台模板目标路径
'web' => 'frontend/web/app/apps',
],
/**
* 资源目录配置
* 插件资源目录名称
*/
'resource' => [
// 数据库迁移目录
'migration' => 'database/migrations',
// 数据库种子目录
'seed' => 'database/seeds',
// 菜单资源目录
'menu' => 'menu',
// 模板资源目录
'template' => 'template',
],
/**
* Logo 配置
*/
'logo' => [
// Logo 存储目录(相对于项目根目录)
'path' => 'resource/images/plugin/logo',
// 默认 logo 文件名
'default' => 'default.png',
// 支持的扩展名
'ext' => ['png', 'jpg', 'jpeg', 'svg', 'webp'],
],
/**
* 依赖合并配置
*/
'dependencies' => [
// 后台依赖配置 (admin/package.json)
'admin' => [
'enabled' => true,
'merge_dev' => true, // 合并 devDependencies
'merge_prod' => true, // 合并 dependencies
],
// 前台依赖配置 (web/package.json)
'web' => [
'enabled' => true,
'merge_dev' => true,
'merge_prod' => true,
],
// 后端依赖配置 (server/composer.json)
'backend' => [
'enabled' => true,
'merge_require' => true, // 合并 require
'merge_require_dev' => true, // 合并 require-dev
],
],
/**
* 数据库配置
*/
'database' => [
// 连接名(默认使用框架配置)
'connection' => null,
],
];
+229
View File
@@ -0,0 +1,229 @@
<?php
namespace core\plugin\traits;
/**
* 配置管理 Trait
* 实现配置的获取、合并和覆盖机制
*/
trait ConfigTrait
{
/**
* 获取应用配置
*/
protected function getAppConfig(): array
{
static $config = null;
if ($config === null) {
// Trait 中 __DIR__ 是 core/plugin/traits,需要两级目录才能到达 core/plugin
$configFile = dirname(dirname(__DIR__)) . '/plugin/config/app.php';
$config = file_exists($configFile) ? include $configFile : [];
}
return $config;
}
/**
* 获取配置项(合并 appConfig + info.php 配置)
* info.php 中的配置优先级更高
*/
protected function getConfig(string $key, mixed $default = null): mixed
{
// 先从 appConfig 获取
$appConfigValue = $this->getAppConfigValue($key);
// 再从插件 info.php 获取(覆盖 appConfig)
$pluginConfigValue = $this->getPluginConfigValue($key);
// 插件配置优先
if ($pluginConfigValue !== null) {
return $pluginConfigValue;
}
return $appConfigValue !== null ? $appConfigValue : $default;
}
/**
* 从 appConfig 获取配置值
*/
private function getAppConfigValue(string $key): mixed
{
$keys = explode('.', $key);
$value = $this->appConfig;
foreach ($keys as $k) {
if (!isset($value[$k])) {
return null;
}
$value = $value[$k];
}
return $value;
}
/**
* 从插件 info.php 获取配置值
*/
private function getPluginConfigValue(string $key): mixed
{
static $pluginInfo = null;
if ($pluginInfo === null) {
$infoFile = $this->getPluginInfoFile();
$pluginInfo = file_exists($infoFile) ? include $infoFile : [];
}
if (empty($pluginInfo)) {
return null;
}
$keys = explode('.', $key);
$value = $pluginInfo;
foreach ($keys as $k) {
if (!isset($value[$k])) {
return null;
}
$value = $value[$k];
}
return $value;
}
/**
* 获取插件信息配置文件路径
*/
protected function getPluginInfoFile(): string
{
return $this->pluginPath . '/config/info.php';
}
/**
* 获取插件信息(包含隐式默认配置)
* info.php 中的配置会覆盖默认配置
*/
protected function getPluginInfo(): array
{
// 先获取显式配置(info.php)
$infoFile = $this->getPluginInfoFile();
$explicitConfig = [];
if (file_exists($infoFile)) {
$explicitConfig = include $infoFile;
$explicitConfig = is_array($explicitConfig) ? $explicitConfig : [];
}
// 合并隐式默认配置
return array_merge($this->getImplicitConfig(), $explicitConfig);
}
/**
* 获取隐式默认配置
*/
protected function getImplicitConfig(): array
{
return [
'dependencies' => [],
'require' => [
'composer' => [],
],
'npm_require' => [
'admin' => [],
'web' => [],
],
'supports' => [
'backend' => true,
'frontend' => true,
'admin' => true,
'web' => true,
],
];
}
/**
* 获取插件配置文件路径
*/
protected function getPluginConfigFile(): string
{
return $this->pluginPath . '/config/installed.php';
}
/**
* 保存插件安装状态
*/
protected function savePluginConfig(string $version): void
{
$configFile = $this->getPluginConfigFile();
$configDir = dirname($configFile);
if (!is_dir($configDir)) {
mkdir($configDir, 0755, true);
}
$existingConfig = [];
if (file_exists($configFile)) {
$existingConfig = include $configFile;
$existingConfig = is_array($existingConfig) ? $existingConfig : [];
}
$config = array_merge($existingConfig, [
'name' => $this->getPluginName(),
'version' => $version,
'installed_at' => date('Y-m-d H:i:s'),
'status' => 'installed',
]);
$content = "<?php\n\n/**\n * 插件安装状态配置\n * 此文件由系统自动生成,请勿手动修改\n */\n\nreturn " . var_export($config, true) . ";\n";
file_put_contents($configFile, $content);
$this->output("📝 Plugin config saved: {$configFile}");
}
/**
* 获取插件安装状态
*/
protected function getPluginConfig(): ?array
{
$configFile = $this->getPluginConfigFile();
if (!file_exists($configFile)) {
return null;
}
$config = include $configFile;
return is_array($config) ? $config : null;
}
/**
* 检查插件是否已安装
*/
public function isInstalled(): bool
{
$config = $this->getPluginConfig();
return $config !== null && ($config['status'] ?? '') === 'installed';
}
/**
* 获取插件版本
*/
public function getVersion(): ?string
{
$config = $this->getPluginConfig();
return $config['version'] ?? null;
}
/**
* 删除插件配置文件
*/
protected function removePluginConfig(): void
{
$configFile = $this->getPluginConfigFile();
if (file_exists($configFile)) {
unlink($configFile);
$this->output("📝 Plugin config removed: {$configFile}");
}
}
}
@@ -0,0 +1,291 @@
<?php
namespace core\plugin\traits;
/**
* 依赖安装 Trait
*/
trait DependencyTrait
{
/**
* 合并 Composer 依赖(后端)
*/
protected function installComposerDeps(): void
{
$pluginInfo = $this->getPluginInfo();
$require = $pluginInfo['require']['composer'] ?? [];
if (empty($require)) {
$this->output("📦 No Composer dependencies to merge");
return;
}
$config = $this->getConfig('dependencies.backend', []);
if (empty($config['enabled'])) {
$this->output("📦 Composer dependency merge disabled");
return;
}
$this->output("📦 Merging Composer dependencies...");
$composerFile = base_path('composer.json');
if (!file_exists($composerFile)) {
$this->output(" ⚠️ composer.json not found");
return;
}
$composerContent = file_get_contents($composerFile);
$composer = json_decode($composerContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->output(" ⚠️ Invalid composer.json format");
return;
}
$merged = false;
// 合并 require
if (!empty($config['merge_require'])) {
foreach ($require as $package => $version) {
if (!isset($composer['require'][$package])) {
$composer['require'][$package] = $version;
$merged = true;
$this->output(" ➕ Added to require: {$package}:{$version}");
}
}
}
// 保存
if ($merged) {
file_put_contents($composerFile, json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
$this->output(" ✅ Composer dependencies merged. Run 'composer install' manually.");
} else {
$this->output(" ℹ️ No new dependencies to merge");
}
}
/**
* 合并 NPM 依赖(前端)
*/
protected function installNpmDeps(): void
{
$pluginInfo = $this->getPluginInfo();
$npmRequire = $pluginInfo['npm_require'] ?? [];
if (empty($npmRequire)) {
$this->output("📦 No NPM dependencies to merge");
return;
}
$projectRoot = $this->getProjectRoot();
// 合并 Admin 端依赖(前端代码统一在 frontend 目录下)
$adminConfig = $this->getConfig('dependencies.admin', []);
if (!empty($adminConfig['enabled'])) {
$adminPath = $this->getFrontendProjectPath('admin');
$this->mergeNpmDeps($npmRequire['admin'] ?? [], $adminPath, 'Admin', $adminConfig);
$this->mergeNpmDeps($npmRequire['common'] ?? [], $adminPath, 'Admin (common)', $adminConfig);
}
// 合并 Web 端依赖
$webConfig = $this->getConfig('dependencies.web', []);
if (!empty($webConfig['enabled'])) {
$webPath = $this->getFrontendProjectPath('web');
$this->mergeNpmDeps($npmRequire['web'] ?? [], $webPath, 'Web', $webConfig);
$this->mergeNpmDeps($npmRequire['common'] ?? [], $webPath, 'Web (common)', $webConfig);
}
}
/**
* 合并 NPM 依赖到 package.json
*/
protected function mergeNpmDeps(array $packages, string $workDir, string $endpoint, array $config): void
{
if (empty($packages) || !is_dir($workDir)) {
return;
}
$packageFile = $workDir . '/package.json';
if (!file_exists($packageFile)) {
$this->output(" ⚠️ {$endpoint}: package.json not found");
return;
}
$packageContent = file_get_contents($packageFile);
$package = json_decode($packageContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->output(" ⚠️ {$endpoint}: Invalid package.json format");
return;
}
$merged = false;
// 合并 dependencies
if (!empty($config['merge_prod'])) {
foreach ($packages as $pkg => $version) {
if (!isset($package['dependencies'][$pkg])) {
$package['dependencies'][$pkg] = $version;
$merged = true;
$this->output(" ➕ {$endpoint}: Added {$pkg}@{$version}");
}
}
}
// 合并 devDependencies
if (!empty($config['merge_dev'])) {
foreach ($packages as $pkg => $version) {
if (!isset($package['devDependencies'][$pkg])) {
$package['devDependencies'][$pkg] = $version;
$merged = true;
$this->output(" ➕ {$endpoint}: Added dev {$pkg}@{$version}");
}
}
}
// 保存
if ($merged) {
file_put_contents($packageFile, json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
$this->output(" ✅ {$endpoint}: Dependencies merged. Run 'npm install' manually.");
}
}
/**
* 移除合并的依赖
*/
protected function removeMergedDependencies(): void
{
$this->output("🗑️ Removing merged dependencies...");
$pluginInfo = $this->getPluginInfo();
// 移除 Composer 依赖
$composerRequire = $pluginInfo['require']['composer'] ?? [];
if (!empty($composerRequire)) {
$this->removeComposerDeps($composerRequire);
}
// 移除 NPM 依赖
$npmRequire = $pluginInfo['npm_require'] ?? [];
if (!empty($npmRequire)) {
// 前端代码统一在 frontend 目录下
$this->removeNpmDeps($npmRequire['admin'] ?? [], $this->getFrontendProjectPath('admin'), 'Admin');
$this->removeNpmDeps($npmRequire['common'] ?? [], $this->getFrontendProjectPath('admin'), 'Admin (common)');
$this->removeNpmDeps($npmRequire['web'] ?? [], $this->getFrontendProjectPath('web'), 'Web');
$this->removeNpmDeps($npmRequire['common'] ?? [], $this->getFrontendProjectPath('web'), 'Web (common)');
}
$this->output(" ✅ Dependencies removed. Run 'composer install' or 'npm install' to update.");
}
/**
* 移除 Composer 依赖
*/
protected function removeComposerDeps(array $packages): void
{
$composerFile = base_path('composer.json');
if (!file_exists($composerFile)) {
return;
}
$composer = json_decode(file_get_contents($composerFile), true);
$removed = false;
foreach ($packages as $package => $version) {
if (isset($composer['require'][$package])) {
unset($composer['require'][$package]);
$removed = true;
$this->output(" ➖ Removed composer: {$package}");
}
}
if ($removed) {
file_put_contents($composerFile, json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
}
}
/**
* 移除 NPM 依赖
*/
protected function removeNpmDeps(array $packages, string $workDir, string $endpoint): void
{
if (empty($packages) || !is_dir($workDir)) {
return;
}
$packageFile = $workDir . '/package.json';
if (!file_exists($packageFile)) {
return;
}
$package = json_decode(file_get_contents($packageFile), true);
$removed = false;
foreach ($packages as $pkg => $version) {
// 移除 dependencies
if (isset($package['dependencies'][$pkg])) {
unset($package['dependencies'][$pkg]);
$removed = true;
$this->output(" ➖ {$endpoint}: Removed {$pkg}");
}
// 移除 devDependencies
if (isset($package['devDependencies'][$pkg])) {
unset($package['devDependencies'][$pkg]);
$removed = true;
$this->output(" ➖ {$endpoint}: Removed dev {$pkg}");
}
}
if ($removed) {
file_put_contents($packageFile, json_encode($package, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
}
}
/**
* 执行 Shell 命令
*/
protected function runShellCommand(string $command, string $workDir, string $label): void
{
if (!is_dir($workDir)) {
$this->output(" ⚠️ {$label}: Directory not found: {$workDir}");
return;
}
$this->output(" 🔧 {$label}: Running '{$command}'");
try {
$descriptorspec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$process = proc_open($command, $descriptorspec, $pipes, $workDir);
if (is_resource($process)) {
stream_get_contents($pipes[1]);
$error = stream_get_contents($pipes[2]);
foreach ($pipes as $pipe) {
fclose($pipe);
}
$exitCode = proc_close($process);
if ($exitCode === 0) {
$this->output(" ✅ {$label}: Completed successfully");
} else {
$this->output(" ❌ {$label}: Failed with exit code {$exitCode}");
if (!empty($error)) {
$this->output(" Error: {$error}");
}
}
}
} catch (\Throwable $e) {
$this->output(" ❌ {$label}: Error - {$e->getMessage()}");
}
}
}
+510
View File
@@ -0,0 +1,510 @@
<?php
namespace core\plugin\traits;
use core\uuid\Snowflake;
use support\Db;
/**
* 菜单操作 Trait
*/
trait MenuTrait
{
/**
* 预加载系统菜单的 code 映射(用于智能关联父级)
*
* @var array
*/
protected array $_admin_menu_codes = [];
protected array $_web_menu_codes = [];
/**
* 加载菜单文件
*/
protected function loadMenus(): void
{
$menuResourceDir = $this->getConfig('resource.menu', 'menu');
$menuDir = $this->pluginPath . '/resource/' . $menuResourceDir;
$this->output("📂 Loading menus from: {$menuDir}");
if (!is_dir($menuDir)) {
$this->output("📂 No menu directory found, skipping...");
return;
}
$files = scandir($menuDir);
$this->output("📂 Menu files found: " . implode(', ', array_diff($files, ['.', '..'])));
// 预加载系统菜单的 code 映射(用于智能关联父级)
$this->preloadSystemMenuCodes('admin');
$this->preloadSystemMenuCodes('web');
// 只加载 admin 和 web 两种类型的菜单
$this->loadMenuFile($menuDir . '/admin.php', 'admin');
$this->loadMenuFile($menuDir . '/web.php', 'web');
}
/**
* 预加载系统菜单的 code 映射
* 用于插件菜单关联到系统已有菜单
*
* @param string $type 菜单类型 (admin/web)
*/
protected function preloadSystemMenuCodes(string $type): void
{
$tableName = $type === 'admin' ? 'sys_menu' : 'web_menu';
$connection = $this->connection ?? null;
try {
// 获取系统中非插件来源的菜单 code -> id 映射
$menus = Db::connection($connection)
->table($tableName)
->where('source', 'system')
->whereNotNull('code')
->where('code', '!=', '')
->select(['id', 'code', 'pid', 'type'])
->get()
->toArray();
$key = $type === 'admin' ? '_admin_menu_codes' : '_web_menu_codes';
$this->$key = [];
foreach ($menus as $menu) {
$code = $menu->code;
$this->$key[$code] = [
'id' => $menu->id,
'pid' => $menu->pid,
'type' => $menu->type,
];
}
$this->output(" 📋 Preloaded {$type} system menus: " . count($this->$key) . " codes");
} catch (\Throwable $e) {
$this->output(" ⚠️ Failed to preload {$type} system menus: {$e->getMessage()}");
}
}
/**
* 根据 pid_code 查找父级 ID
*
* @param string|null $pidCode 父级菜单的 code
* @param string $type 菜单类型
* @return int 父级 ID,0 表示顶级菜单
*/
protected function findParentIdByCode(?string $pidCode, string $type): int
{
if (empty($pidCode)) {
return 0;
}
$key = $type === 'admin' ? '_admin_menu_codes' : '_web_menu_codes';
$codes = $this->$key ?? [];
if (!isset($codes[$pidCode])) {
return 0;
}
$parentInfo = $codes[$pidCode];
// 如果父级是目录类型(type=1),则返回其 ID
// 否则作为顶级菜单
if ($type === 'admin') {
// admin: 1=目录 2=菜单 3=按钮
return in_array($parentInfo['type'], [1]) ? $parentInfo['id'] : 0;
} else {
// web 菜单类型判断
return $parentInfo['id'];
}
}
/**
* 加载单个菜单文件
*/
protected function loadMenuFile(string $filePath, string $type): void
{
if (!file_exists($filePath)) {
$this->output(" ⚠️ {$type}: Menu file not found");
return;
}
$menu = include $filePath;
if (empty($menu)) {
$this->output(" ⚠️ {$type}: Empty menu file");
return;
}
if (!is_array($menu)) {
$this->output(" ❌ {$type}: Invalid menu format");
return;
}
$this->saveMenuToSystem($type, $menu);
$this->output(" ✅ {$type}: Menu loaded (" . count($menu) . " items)");
}
/**
* 保存菜单到系统
*/
protected function saveMenuToSystem(string $type, array $menu): void
{
if ($type === 'admin') {
$this->saveAdminMenu($menu);
} else {
$this->saveWebMenu($menu);
}
}
/**
* 保存后台菜单
*/
protected function saveAdminMenu(array $menu): void
{
$tableName = 'sys_menu';
$this->output(" 📝 Saving admin menu to {$tableName}...");
try {
$connection = $this->connection ?? null;
$hasTable = Db::connection($connection)->getSchemaBuilder()->hasTable($tableName);
if (!$hasTable) {
$this->output(" ⚠️ Table {$tableName} does not exist, skipping menu import");
return;
}
$this->insertAdminMenuItems($tableName, $menu, $this->pluginName);
$this->output(" ✅ Admin menu saved to {$tableName}");
} catch (\Throwable $e) {
$this->output(" ❌ Failed to save admin menu: {$e->getMessage()}");
}
}
/**
* 插入后台菜单项
*
* @param string $tableName 表名
* @param array $items 菜单项
* @param string $plugin 插件标识
* @param int $parentId 父级ID
* @param int $level 当前层级
*/
protected function insertAdminMenuItems(string $tableName, array $items, string $plugin, int $parentId = 0, int $level = 1): void
{
$connection = $this->connection ?? null;
foreach ($items as $index => $item) {
$menuId = (int)Snowflake::generate();
// 智能获取父级 ID(仅顶级菜单使用 pid_code)
// 子菜单通过 parentId(children 嵌套)确定父级
if ($parentId === 0 && isset($item['pid_code'])) {
$parentId = $this->findParentIdByCode($item['pid_code'], 'admin');
}
// 规范化菜单数据:自动生成缺失的 code
$normalizedItem = $this->normalizeMenuItem($item, $plugin, $level, $index);
$data = [
'id' => $menuId,
'pid' => $parentId,
'app' => $item['app'] ?? $plugin,
'source' => 'plugin',
'title' => $normalizedItem['title'],
'code' => $normalizedItem['code'],
'level' => $level,
'type' => $normalizedItem['type'],
'sort' => $normalizedItem['sort'],
'path' => $normalizedItem['path'],
'component' => $normalizedItem['component'],
'icon' => $normalizedItem['icon'],
'is_show' => $normalizedItem['is_show'],
'is_link' => $normalizedItem['is_link'],
'is_cache' => $normalizedItem['is_cache'],
'is_sync' => $normalizedItem['is_sync'],
'created_at' => time(),
'updated_at' => time(),
];
Db::connection($connection)->table($tableName)->insert($data);
// 记录本次插入菜单的 code -> id 映射(供子菜单使用)
if ($normalizedItem['code']) {
$this->_admin_menu_codes[$normalizedItem['code']] = [
'id' => $menuId,
'pid' => $parentId,
'type' => $data['type'],
];
}
// 递归处理子菜单,层级 +1
if (!empty($item['children'])) {
$this->insertAdminMenuItems($tableName, $item['children'], $plugin, $menuId, $level + 1);
}
}
}
/**
* 规范化菜单项数据
* 确保必要字段存在,自动生成缺失的 code
*
* @param array $item 菜单项
* @param string $plugin 插件标识
* @param int $level 当前层级
* @param int $index 当前索引
* @return array 规范化后的菜单项
*/
protected function normalizeMenuItem(array $item, string $plugin, int $level, int $index): array
{
// 获取菜单名称
$title = $item['name'] ?? $item['title'] ?? '';
// 获取路径
$path = $item['path'] ?? '';
// 自动生成 code:如果没有 code 字段,根据插件名和路径生成
$code = $item['code'] ?? '';
if (empty($code)) {
// 从 path 生成 code: /test-plugin/index -> test-plugin:index
if (!empty($path)) {
$code = trim($path, '/');
$code = str_replace('/', ':', $code);
}
// 如果路径也为空,使用插件名+层级+索引生成唯一 code
if (empty($code)) {
$code = $plugin . ':level' . $level . '_' . $index;
}
}
return [
'title' => $title,
'code' => $code,
'type' => $item['type'] ?? ($level === 1 ? 1 : 2), // 1=目录 2=菜单
'sort' => $item['sort'] ?? 0,
'path' => $path,
'component' => $item['component'] ?? ($level === 1 ? '/layout' : ''),
'icon' => $item['icon'] ?? '',
'is_show' => $item['is_show'] ?? 1,
'is_link' => $item['is_link'] ?? 0,
'is_cache' => $item['is_cache'] ?? 0,
'is_sync' => $item['is_sync'] ?? 0,
];
}
/**
* 保存前台菜单
*/
protected function saveWebMenu(array $menu): void
{
$tableName = 'web_menu';
$this->output(" 📝 Saving web menu to {$tableName}...");
try {
$connection = $this->connection ?? null;
$hasTable = Db::connection($connection)->getSchemaBuilder()->hasTable($tableName);
if (!$hasTable) {
$this->output(" ⚠️ Table {$tableName} does not exist, skipping menu import");
return;
}
$this->insertWebMenuItems($tableName, $menu, $this->pluginName);
$this->output(" ✅ Web menu saved to {$tableName}");
} catch (\Throwable $e) {
$this->output(" ❌ Failed to save web menu: {$e->getMessage()}");
}
}
/**
* 插入前台菜单项
*
* @param string $tableName 表名
* @param array $items 菜单项
* @param string $plugin 插件标识
* @param int $parentId 父级ID
* @param int $level 当前层级
*/
protected function insertWebMenuItems(string $tableName, array $items, string $plugin, int $parentId = 0, int $level = 1): void
{
$connection = $this->connection ?? null;
foreach ($items as $index => $item) {
$menuId = (int)Snowflake::generate();
// 智能获取父级 ID(仅顶级菜单使用 pid_code)
// 子菜单通过 parentId(children 嵌套)确定父级
if ($parentId === 0 && isset($item['pid_code'])) {
$parentId = $this->findParentIdByCode($item['pid_code'], 'web');
}
// 规范化菜单数据:自动生成缺失的 code
$normalizedItem = $this->normalizeWebMenuItem($item, $plugin, $level, $index);
// target: 1=当前窗口 2=新窗口
$targetValue = 1; // 默认当前窗口
if (isset($item['target'])) {
$targetValue = $item['target'] === '_self' || $item['target'] === 1 || $item['target'] === '1' ? 1 : 2;
}
$data = [
'id' => $menuId,
'app' => $item['app'] ?? $plugin,
'category' => $item['category'] ?? '1',
'source' => 'plugin',
'code' => $normalizedItem['code'],
'name' => $normalizedItem['name'],
'url' => $normalizedItem['url'],
'pid' => $parentId,
'level' => $level,
'type' => $normalizedItem['type'],
'sort' => $normalizedItem['sort'],
'target' => $targetValue,
'icon' => $normalizedItem['icon'],
'is_show' => $normalizedItem['is_show'],
'enabled' => $normalizedItem['enabled'],
'created_at' => time(),
'updated_at' => time(),
];
Db::connection($connection)->table($tableName)->insert($data);
// 记录本次插入菜单的 code -> id 映射(供子菜单使用)
if ($normalizedItem['code']) {
$this->_web_menu_codes[$normalizedItem['code']] = [
'id' => $menuId,
'pid' => $parentId,
'type' => $data['type'],
];
}
// 递归处理子菜单,层级 +1
if (!empty($item['children'])) {
$this->insertWebMenuItems($tableName, $item['children'], $plugin, $menuId, $level + 1);
}
}
}
/**
* 规范化前台菜单项数据
* 确保必要字段存在,自动生成缺失的 code
*
* @param array $item 菜单项
* @param string $plugin 插件标识
* @param int $level 当前层级
* @param int $index 当前索引
* @return array 规范化后的菜单项
*/
protected function normalizeWebMenuItem(array $item, string $plugin, int $level, int $index): array
{
// 获取菜单名称
$name = $item['name'] ?? $item['title'] ?? '';
// 获取 URL
$url = $item['path'] ?? $item['url'] ?? '';
// 自动生成 code:如果没有 code 字段,根据插件名和 URL 生成
$code = $item['code'] ?? '';
if (empty($code)) {
// 从 url 生成 code
if (!empty($url)) {
$code = trim($url, '/');
$code = str_replace('/', ':', $code);
}
// 如果 URL 也为空,使用插件名+层级+索引生成唯一 code
if (empty($code)) {
$code = $plugin . ':level' . $level . '_' . $index;
}
}
return [
'name' => $name,
'code' => $code,
'url' => $url,
'type' => $item['type'] ?? 1,
'sort' => $item['sort'] ?? 0,
'icon' => $item['icon'] ?? '',
'is_show' => $item['is_show'] ?? 1,
'enabled' => $item['enabled'] ?? 1,
];
}
/**
* 清除插件菜单
*/
protected function clearPluginMenus(string $plugin): void
{
$this->clearMenusByTable('sys_menu', $plugin);
$this->clearMenusByTable('web_menu', $plugin);
}
/**
* 根据表清除菜单
*/
protected function clearMenusByTable(string $tableName, string $plugin): void
{
try {
$connection = $this->connection ?? null;
if (!Db::connection($connection)->getSchemaBuilder()->hasTable($tableName)) {
return;
}
$query = Db::connection($connection)
->table($tableName)
->where('source', 'plugin')
->where(function ($query) use ($tableName, $plugin) {
$query->where('app', $plugin)
->orWhere('code', 'like', $plugin . ':%');
if ($tableName === 'sys_menu') {
$query->orWhere('path', 'like', '/' . $plugin . '%')
->orWhere('component', 'like', '/' . $plugin . '/%');
} else {
$query->orWhere('url', 'like', '/' . $plugin . '%');
}
});
$deleted = $query->delete();
if ($deleted > 0) {
$this->output(" 🗑️ Cleared {$deleted} menus from {$tableName}");
}
} catch (\Throwable $e) {
$this->output(" ⚠️ Failed to clear {$tableName}: {$e->getMessage()}");
}
}
/**
* 导入插件菜单(快捷方法)
*/
public function importPluginMenus(): void
{
$this->importMenus();
}
/**
* 卸载插件菜单(快捷方法)
*/
public function uninstallPluginMenus(): void
{
$this->clearMenus();
}
/**
* 导入菜单(子类可重写)
*/
protected function importMenus(): void
{
$this->loadMenus();
}
/**
* 清除菜单(子类可重写)
*/
protected function clearMenus(): void
{
$this->clearPluginMenus($this->pluginName);
}
}
@@ -0,0 +1,668 @@
<?php
namespace core\plugin\traits;
use Illuminate\Database\Schema\Builder;
use support\Db;
/**
* 迁移操作 Trait
*/
trait MigrationTrait
{
/**
* 插件临时目录路径
*/
const RUNTIME_PLUGIN_PATH = 'install/plugin';
/**
* 运行迁移
*/
public function runMigrations(): void
{
$migrationsDir = $this->findMigrationsDir();
if (!$migrationsDir) {
$this->output("⚠️ No migrations directory found");
return;
}
$this->output("📁 Migrations dir: {$migrationsDir}");
// 获取已执行的迁移
$executed = $this->getExecutedMigrations();
// 获取所有迁移文件
$files = glob($migrationsDir . '/*.php');
$pending = [];
foreach ($files as $file) {
$basename = basename($file, '.php');
if (!in_array($basename, $executed)) {
$pending[] = [$file, $basename];
}
}
sort($pending);
if (empty($pending)) {
$this->output("✅ No pending migrations");
return;
}
$this->output("📋 Pending migrations: " . count($pending));
$batch = $this->getNextBatch();
$schema = $this->getSchemaBuilder();
// 直接执行迁移
foreach ($pending as $migration) {
$this->runMigration($migration[0], $migration[1], $schema);
}
// 记录批次
$this->recordBatch($batch, $pending);
$this->output("✅ Migrations completed (batch: {$batch})");
}
/**
* 运行单个迁移
*/
protected function runMigration(string $file, string $name, Builder $schema): void
{
$this->output(" 📝 Migrating: {$name}");
try {
$migration = require $file;
$migration->up($schema);
$this->output(" ✅ Migrated: {$name}");
} catch (\Throwable $e) {
$this->output(" ❌ Error: {$e->getMessage()}");
throw $e;
}
}
/**
* 回滚迁移
* @param bool $dropTables 是否删除表
*/
public function rollbackMigrations(bool $dropTables = false): void
{
if ($dropTables) {
$this->dropAllTables();
$this->saveMigrationRows([]);
$this->output("✅ All plugin tables dropped");
return;
}
// 不删除表,只清理迁移日志
$this->saveMigrationRows([]);
$this->output("📝 Migration records cleared (tables preserved)");
}
/**
* 删除插件所有表
*/
protected function dropAllTables(): void
{
$tables = $this->getPluginTables();
if (empty($tables)) {
$this->output(" ⚠️ No tables to drop");
return;
}
$schema = $this->getSchemaBuilder();
foreach ($tables as $table) {
$this->output(" 🗑️ Dropping table: {$table}");
$schema->dropIfExists($table);
}
}
/**
* 获取插件相关的所有表
*/
protected function getPluginTables(): array
{
$tables = [];
$migrationsDir = $this->findMigrationsDir();
// 从迁移文件解析表名
if (is_dir($migrationsDir)) {
$files = glob($migrationsDir . '/*.php');
foreach ($files as $file) {
$content = file_get_contents($file);
if (preg_match_all('/->create\([\'"](\w+)[\'"]/', $content, $matches)) {
$tables = array_merge($tables, $matches[1]);
}
}
}
// 从数据库查询以插件名前缀的表
$prefix = $this->pluginName . '_';
try {
$connection = $this->connection ?? null;
$db = Db::connection($connection)->getDoctrineConnection();
$tablesList = $db->getSchemaManager()->listTableNames();
foreach ($tablesList as $tableName) {
if (strpos($tableName, $prefix) === 0) {
$tables[] = $tableName;
}
}
} catch (\Throwable $e) {
// 忽略查询错误
}
return array_unique($tables);
}
/**
* 回滚单个迁移
*/
protected function rollbackMigration(string $name, Builder $schema): void
{
$this->output(" 📝 Rolling back: {$name}");
$migrationsDir = $this->findMigrationsDir();
$file = $migrationsDir . '/' . $name . '.php';
if (!file_exists($file)) {
$this->output(" ⚠️ Migration file not found: {$file}");
return;
}
try {
$migration = require $file;
$migration->down($schema);
$this->output(" ✅ Rolled back: {$name}");
} catch (\Throwable $e) {
$this->output(" ❌ Error: {$e->getMessage()}");
}
}
/**
* 查找迁移目录
*/
protected function findMigrationsDir(): ?string
{
$resourceDir = $this->getConfig('resource.migration', 'database/migrations');
$dirs = [
$this->pluginPath . '/resource/database/migrations',
$this->pluginPath . '/' . $resourceDir,
$this->pluginPath . '/install/migrations',
base_path("resource/database/migrations/plugin/{$this->pluginName}"),
];
foreach ($dirs as $dir) {
if (is_dir($dir)) {
return $dir;
}
}
return null;
}
/**
* 获取 Schema Builder
*/
protected function getSchemaBuilder(): Builder
{
return Db::connection($this->connection ?? null)->getSchemaBuilder();
}
/**
* 获取迁移日志文件
*/
protected function getMigrationLogFile(): string
{
return runtime_path(self::RUNTIME_PLUGIN_PATH . '/' . $this->pluginName . '-migrations.log');
}
/**
* 获取已执行的迁移
*/
protected function getExecutedMigrations(): array
{
$logFile = $this->getMigrationLogFile();
if (!file_exists($logFile)) {
return [];
}
$content = trim(file_get_contents($logFile));
if (empty($content)) {
return [];
}
return array_filter(explode(PHP_EOL, $content));
}
/**
* 获取迁移记录行
*/
protected function getMigrationRows(): array
{
$logFile = $this->getMigrationLogFile();
if (!file_exists($logFile)) {
return [];
}
$content = trim(file_get_contents($logFile));
if (empty($content)) {
return [];
}
$rows = [];
foreach (explode(PHP_EOL, $content) as $line) {
$parts = explode(',', $line, 2);
if (count($parts) === 2) {
$rows[] = $parts;
}
}
return $rows;
}
/**
* 保存迁移记录行
*/
protected function saveMigrationRows(array $rows): void
{
$logFile = $this->getMigrationLogFile();
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
if (empty($rows)) {
@unlink($logFile);
return;
}
$lines = array_map(fn($r) => "{$r[0]},{$r[1]}", $rows);
file_put_contents($logFile, implode(PHP_EOL, $lines) . PHP_EOL, LOCK_EX);
}
/**
* 获取下一批次号
*/
protected function getNextBatch(): int
{
$rows = $this->getMigrationRows();
if (empty($rows)) {
return 1;
}
$batches = array_map(fn($row) => (int)$row[0], $rows);
return max($batches) + 1;
}
/**
* 记录批次
*/
protected function recordBatch(int $batch, array $migrations): void
{
$logFile = $this->getMigrationLogFile();
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$lines = array_map(fn($m) => "{$batch},{$m[1]}", $migrations);
file_put_contents($logFile, PHP_EOL . implode(PHP_EOL, $lines), FILE_APPEND | LOCK_EX);
}
/**
* 清理日志
*/
protected function clearLogs(): void
{
@unlink($this->getMigrationLogFile());
@unlink($this->getSeedLogFile());
}
/**
* 导出 SQL(从迁移文件生成)
*
* @param bool $withData 是否包含数据
* @param string|null $outputFile 输出文件路径(null 则输出到插件目录)
* @return bool 是否成功
*/
public function exportSql(bool $withData = false, ?string $outputFile = null): bool
{
$migrationsDir = $this->findMigrationsDir();
if (!$migrationsDir || !is_dir($migrationsDir)) {
$this->output("⚠️ No migrations directory found");
return false;
}
$files = glob($migrationsDir . '/*.php');
if (empty($files)) {
$this->output("⚠️ No migration files found");
return false;
}
$this->output("📤 Exporting SQL from " . count($files) . " migration file(s)...");
// 从迁移文件生成 SQL
$sql = $this->generateSqlFromMigrations($files, $withData);
// 默认输出到插件的 resource/database 目录
if (!$outputFile) {
$outputDir = $this->pluginPath . '/resource/database';
if (!is_dir($outputDir)) {
mkdir($outputDir, 0755, true);
}
$filename = $withData ? 'install.sql' : 'structure.sql';
$outputFile = $outputDir . '/' . $filename;
}
$dir = dirname($outputFile);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($outputFile, $sql);
$this->output("✅ SQL exported to: {$outputFile}");
return true;
}
/**
* 从迁移文件生成 SQL
*/
protected function generateSqlFromMigrations(array $files, bool $withData): string
{
$sql = "-- Plugin: {$this->pluginName}\n";
$sql .= "-- Exported at: " . date('Y-m-d H:i:s') . "\n";
$sql .= "-- Generated from migration files\n\n";
$sql .= "SET FOREIGN_KEY_CHECKS = 0;\n\n";
// 按文件名排序
sort($files);
foreach ($files as $file) {
$basename = basename($file, '.php');
$this->output(" 📤 Processing: {$basename}");
$sql .= $this->generateSqlFromMigrationFile($file, $withData);
$sql .= "\n";
}
$sql .= "SET FOREIGN_KEY_CHECKS = 1;\n";
return $sql;
}
/**
* 从单个迁移文件生成 SQL
*/
protected function generateSqlFromMigrationFile(string $file, bool $withData): string
{
$sql = "";
$content = file_get_contents($file);
// 提取所有表名
preg_match_all('/->create\([\'"](\w+)[\'"]\s*,/', $content, $tableMatches);
if (empty($tableMatches[1])) {
return $sql;
}
foreach ($tableMatches[1] as $tableName) {
$sql .= "-- ----------------------------\n";
$sql .= "-- Table structure for {$tableName}\n";
$sql .= "-- ----------------------------\n";
$sql .= "DROP TABLE IF EXISTS `{$tableName}`;\n";
// 从迁移代码提取 CREATE TABLE 部分
// 匹配 ->create('table_name', function (Blueprint $table) { ... });
$pattern = '/->create\([\'"]' . preg_quote($tableName, '/') . '[\'"]\s*,\s*function\s*\([^)]*\)\s*\{([\s\S]*?)\}\s*\)/';
if (preg_match($pattern, $content, $matches)) {
$body = $matches[1];
$sql .= $this->generateCreateTableFromBlueprint($tableName, $body);
}
$sql .= ";\n\n";
// 如果需要数据,尝试从数据库获取(如果表存在)
if ($withData) {
$sql .= $this->generateInsertDataSql($tableName);
$sql .= "\n";
}
}
return $sql;
}
/**
* 从 Blueprint 代码生成 CREATE TABLE
*/
protected function generateCreateTableFromBlueprint(string $tableName, string $body): string
{
$columns = [];
$indexes = [];
// 匹配 $table->列方法
// $table->id();
// $table->string('name', 100);
// $table->text('content')->nullable();
// $table->foreignId('user_id')->references('id')->on('users');
// 匹配各种列定义
$lines = explode("\n", $body);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line) || $line === '$table->') {
continue;
}
// 移除 $table-> 前缀和结尾分号
$line = ltrim($line, '$table->');
$line = rtrim($line, ';');
// 匹配方法调用
if (preg_match('/^(\w+)\s*\(([^)]*)\)(.*)/', $line, $matches)) {
$method = $matches[1];
$args = $matches[2];
$modifiers = $matches[3] ?? '';
$colDef = $this->parseBlueprintMethod($method, $args, $modifiers);
if ($colDef) {
$columns[] = $colDef;
}
}
}
if (empty($columns)) {
return "CREATE TABLE `{$tableName}` ()";
}
return "CREATE TABLE `{$tableName}` (\n " . implode(",\n ", $columns) . "\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
}
/**
* 解析 Blueprint 方法
*/
protected function parseBlueprintMethod(string $method, string $args, string $modifiers): ?string
{
// 解析参数
$argList = [];
if ($args) {
// 简单解析逗号分隔的参数
$depth = 0;
$current = '';
for ($i = 0; $i < strlen($args); $i++) {
$char = $args[$i];
if ($char === '(') {
$depth++;
$current .= $char;
} elseif ($char === ')') {
$depth--;
$current .= $char;
} elseif ($char === ',' && $depth === 0) {
$argList[] = trim($current);
$current = '';
} else {
$current .= $char;
}
}
if ($current) {
$argList[] = trim($current);
}
}
// 处理修饰符
$nullable = strpos($modifiers, '->nullable()') !== false;
$default = null;
if (preg_match('/->default\(([^)]+)\)/', $modifiers, $dm)) {
$default = $dm[1];
}
$autoIncrement = strpos($modifiers, '->autoIncrement()') !== false
|| strpos($modifiers, '->autoIncrements()') !== false;
$primary = strpos($modifiers, '->primary()') !== false;
// 根据方法名生成列定义
return match ($method) {
'id' => $this->formatColumn('id', 'bigint', $nullable, $default, $autoIncrement, $primary),
'bigIncrements' => $this->formatColumn(null, 'bigint', false, null, true, true),
'bigInteger' => $this->formatColumn($argList[0] ?? null, 'bigint', $nullable, $default, $autoIncrement, $primary),
'integer', 'increments', 'mediumIncrements', 'smallIncrements', 'tinyIncrements' =>
$this->formatColumn($argList[0] ?? null, 'int', $nullable, $default, $autoIncrement, $primary),
'bigInteger' => $this->formatColumn($argList[0] ?? null, 'bigint', $nullable, $default, $autoIncrement, $primary),
'string' => $this->formatColumn($argList[0] ?? null, 'varchar(' . ($argList[1] ?? 255) . ')', $nullable, $default, false, false),
'text' => $this->formatColumn($argList[0] ?? null, 'text', $nullable, $default, false, false),
'longText' => $this->formatColumn($argList[0] ?? null, 'longtext', $nullable, $default, false, false),
'mediumText' => $this->formatColumn($argList[0] ?? null, 'mediumtext', $nullable, $default, false, false),
'json' => $this->formatColumn($argList[0] ?? null, 'json', $nullable, $default, false, false),
'boolean' => $this->formatColumn($argList[0] ?? null, 'tinyint(1)', $nullable, $default, false, false),
'date' => $this->formatColumn($argList[0] ?? null, 'date', $nullable, $default, false, false),
'datetime' => $this->formatColumn($argList[0] ?? null, 'datetime', $nullable, $default, false, false),
'timestamp' => $this->formatColumn($argList[0] ?? null, 'timestamp', $nullable, $default, false, false),
'time' => $this->formatColumn($argList[0] ?? null, 'time', $nullable, $default, false, false),
'float' => $this->formatColumn($argList[0] ?? null, 'float', $nullable, $default, false, false),
'double' => $this->formatColumn($argList[0] ?? null, 'double', $nullable, $default, false, false),
'decimal' => $this->formatColumn($argList[0] ?? null, 'decimal(' . ($argList[1] ?? '10,2') . ')', $nullable, $default, false, false),
'enum' => $this->formatColumn($argList[0] ?? null, 'enum(' . ($argList[1] ?? '') . ')', $nullable, $default, false, false),
'uuid' => $this->formatColumn($argList[0] ?? null, 'char(36)', $nullable, $default, false, false),
'foreignId', 'foreignIdFor' => $this->formatColumn($argList[0] ?? null, 'bigint', $nullable, $default, false, false),
'foreignUuid' => $this->formatColumn($argList[0] ?? null, 'char(36)', $nullable, $default, false, false),
'rememberToken' => $this->formatColumn('remember_token', 'varchar(100)', true, null, false, false),
'timestamps' => "`created_at` datetime NULL,\n `updated_at` datetime NULL",
'timestampable' => "`created_at` datetime NULL,\n `updated_at` datetime NULL",
'softDeletes' => "`deleted_at` datetime NULL",
'softDeletesTz' => "`deleted_at` datetime NULL",
default => null,
};
}
/**
* 格式化列定义
*/
protected function formatColumn(?string $name, string $type, bool $nullable, ?string $default, bool $autoIncrement, bool $primary): ?string
{
if (!$name) {
return null;
}
$col = "`{$name}` {$type}";
if ($nullable) {
$col .= " NULL";
} else {
$col .= " NOT NULL";
}
if ($autoIncrement) {
$col .= " AUTO_INCREMENT";
}
if ($default !== null) {
$col .= " DEFAULT " . $default;
}
if ($primary) {
$col .= ", PRIMARY KEY (`{$name}`)";
}
return $col;
}
/**
* 生成 INSERT DATA SQL(从数据库)
*/
protected function generateInsertDataSql(string $table): string
{
$sql = "-- ----------------------------\n";
$sql .= "-- Data for {$table}\n";
$sql .= "-- ----------------------------\n";
try {
$connection = $this->connection ?? null;
$pdo = Db::connection($connection)->getPdo();
// 检查表是否存在
$stmt = $pdo->query("SHOW TABLES LIKE '{$table}'");
if ($stmt->rowCount() === 0) {
$sql .= "-- Table does not exist, skipping data\n";
return $sql;
}
$stmt = $pdo->query("SELECT * FROM `{$table}`");
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
if (empty($rows)) {
$sql .= "-- No data\n";
return $sql;
}
foreach ($rows as $row) {
$values = array_map([$this, 'quoteValue'], array_values($row));
$sql .= "INSERT INTO `{$table}` (`" . implode('`, `', array_keys($row)) . "`) VALUES (" . implode(', ', $values) . ");\n";
}
} catch (\Throwable $e) {
$sql .= "-- Error: {$e->getMessage()}\n";
}
return $sql;
}
/**
* 转义值
*/
protected function quoteValue(mixed $value): string
{
if ($value === null) {
return 'NULL';
}
if (is_bool($value)) {
return $value ? '1' : '0';
}
if (is_int($value) || is_float($value)) {
return (string)$value;
}
// 使用 PDO 的 quote
$connection = $this->connection ?? null;
$pdo = Db::connection($connection)->getPdo();
return $pdo->quote($value);
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
namespace core\plugin\traits;
/**
* 种子操作 Trait
*/
trait SeedTrait
{
/**
* 插件临时目录路径
*/
const RUNTIME_PLUGIN_PATH = 'install/plugin';
/**
* 运行种子
*/
public function runSeeds(): void
{
$seedsDir = $this->findSeedsDir();
if (!$seedsDir) {
$this->output("⚠️ No seeds directory found");
return;
}
$this->output("📁 Seeds dir: {$seedsDir}");
$executed = $this->getExecutedSeeds();
$files = glob($seedsDir . '/*.php');
$pending = [];
foreach ($files as $file) {
$basename = basename($file, '.php');
$seedName = str_replace('Seeder', '', $basename);
if (!in_array($seedName, $executed)) {
$pending[] = [$file, $basename];
}
}
sort($pending);
if (empty($pending)) {
$this->output("✅ No pending seeds");
return;
}
$this->output("📋 Pending seeds: " . count($pending));
foreach ($pending as $seed) {
$this->runSeed($seed[0], $seed[1]);
}
$this->output("✅ Seeds completed");
}
/**
* 运行单个种子
*/
protected function runSeed(string $file, string $className): void
{
$this->output(" 📝 Seeding: {$className}");
require_once $file;
$seeder = null;
$classes = get_declared_classes();
foreach ($classes as $cls) {
if (str_ends_with($cls, '\\' . $className) || $cls === $className) {
$seeder = new $cls();
break;
}
}
if (!$seeder) {
$this->output(" ❌ Seeder class not found: {$className}");
return;
}
try {
if (method_exists($seeder, 'run')) {
$seeder->run();
}
$this->output(" ✅ Seeded: {$className}");
$this->recordSeed(str_replace('Seeder', '', $className));
} catch (\Throwable $e) {
$this->output(" ❌ Error: {$e->getMessage()}");
}
}
/**
* 查找种子目录
*/
protected function findSeedsDir(): ?string
{
$resourceDir = $this->getConfig('resource.seed', 'database/seeds');
$dirs = [
$this->pluginPath . '/resource/database/seeds',
$this->pluginPath . '/' . $resourceDir,
$this->pluginPath . '/install/seeds',
base_path("resource/database/seeds/plugin/{$this->pluginName}"),
];
foreach ($dirs as $dir) {
if (is_dir($dir)) {
return $dir;
}
}
return null;
}
/**
* 获取种子日志文件
*/
protected function getSeedLogFile(): string
{
return runtime_path(self::RUNTIME_PLUGIN_PATH . '/' . $this->pluginName . '-seeds.log');
}
/**
* 获取已执行的种子
*/
protected function getExecutedSeeds(): array
{
$logFile = $this->getSeedLogFile();
if (!file_exists($logFile)) {
return [];
}
$content = trim(file_get_contents($logFile));
if (empty($content)) {
return [];
}
return array_filter(explode(PHP_EOL, $content));
}
/**
* 记录种子
*/
protected function recordSeed(string $name): void
{
$logFile = $this->getSeedLogFile();
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
file_put_contents($logFile, PHP_EOL . $name, FILE_APPEND | LOCK_EX);
}
}
@@ -0,0 +1,179 @@
<?php
namespace core\plugin\traits;
/**
* 模板资源操作 Trait
*/
trait TemplateTrait
{
/**
* 复制模板资源到目标目录
*/
protected function copyTemplates(): void
{
$templateDir = $this->pluginPath . '/resource/template';
if (!is_dir($templateDir)) {
$this->output("📂 No template directory found, skipping...");
return;
}
$this->output("📂 Copying templates from {$templateDir}...");
$this->copyTemplateDir('admin');
$this->copyTemplateDir('web');
}
/**
* 复制单个端的模板
*/
protected function copyTemplateDir(string $endpoint): void
{
$resourceDir = $this->getConfig('resource.template', 'template');
$targetBase = $this->getConfig("template.{$endpoint}");
$sourceDir = $this->pluginPath . '/resource/' . $resourceDir . '/' . $endpoint;
$targetDir = $this->getProjectRoot() . '/' . $targetBase . '/' . $this->pluginName;
if (!is_dir($sourceDir)) {
$this->output(" ⚠️ {$endpoint}: Template source not found");
return;
}
if (!is_dir(dirname($targetDir))) {
mkdir(dirname($targetDir), 0755, true);
}
$this->recurseCopy($sourceDir, $targetDir);
$this->output(" ✅ {$endpoint}: Templates copied to {$targetDir}");
}
/**
* 递归复制目录
*/
protected function recurseCopy(string $source, string $target): void
{
if (!is_dir($source)) {
return;
}
if (!is_dir($target)) {
mkdir($target, 0755, true);
}
$files = scandir($source);
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$sourcePath = $source . '/' . $file;
$targetPath = $target . '/' . $file;
if (is_dir($sourcePath)) {
$this->recurseCopy($sourcePath, $targetPath);
} else {
copy($sourcePath, $targetPath);
}
}
}
/**
* 删除插件模板资源
*/
protected function deleteTemplates(): void
{
if ($this->preserveTemplatesDuringUninstall) {
$this->output('📦 Template resources are preserved during uninstall.');
return;
}
$this->output("🗑️ Deleting plugin templates...");
$this->deleteTemplateDir('admin');
$this->deleteTemplateDir('web');
}
/**
* 删除单个端模板
*/
protected function deleteTemplateDir(string $endpoint): void
{
$targetBase = $this->getConfig("template.{$endpoint}");
$targetDir = $this->getProjectRoot() . '/' . $targetBase . '/' . $this->pluginName;
if (!is_dir($targetDir)) {
$this->output(" ⚠️ {$endpoint}: Template directory not found");
return;
}
$this->recurseDelete($targetDir);
$this->output(" ✅ {$endpoint}: Templates deleted");
}
/**
* 递归删除目录
*/
protected function recurseDelete(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = scandir($dir);
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$path = $dir . '/' . $file;
if (is_dir($path)) {
$this->recurseDelete($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
/**
* 导入模板(子类可重写)
*/
protected function importTemplates(): void
{
$this->copyTemplates();
}
/**
* 获取项目根目录
*/
protected function getProjectRoot(): string
{
return dirname(base_path());
}
/**
* 获取前端基础目录
* 前端代码统一在 frontend 目录下
*/
protected function getFrontendPath(): string
{
return $this->getProjectRoot() . DIRECTORY_SEPARATOR . 'frontend';
}
/**
* 获取前端指定项目目录
*
* @param string $frontendType 前端类型(admin/web/uni-app)
* @return string 前端项目目录
*/
protected function getFrontendProjectPath(string $frontendType): string
{
return $this->getFrontendPath() . DIRECTORY_SEPARATOR . $frontendType;
}
}