初始化本地仓库
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use core\tool\ApiEncryptService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'api-encrypt:generate-keypair',
|
||||
description: 'Generate API encryption RSA keypair',
|
||||
aliases: ['api-encrypt:generate-keypair'],
|
||||
hidden: false
|
||||
)]
|
||||
class ApiEncryptGenerateKeypairCommand extends BaseCommand
|
||||
{
|
||||
public function __construct(protected ApiEncryptService $service)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
try {
|
||||
$keys = $this->service->generateKeyPair();
|
||||
$this->service->persistKeyPair($keys);
|
||||
$io->success('接口加密密钥对生成成功');
|
||||
return Command::SUCCESS;
|
||||
} catch (\Throwable $exception) {
|
||||
$io->error($exception->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 命令基类
|
||||
*
|
||||
* 提供通用的工具方法供所有命令使用
|
||||
* - SSE 事件处理:适用于需要流式输出的命令(如插件安装/卸载)
|
||||
* - 通用工具方法:目录操作、确认、输出等
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
abstract class BaseCommand extends Command
|
||||
{
|
||||
/**
|
||||
* 解析SSE事件
|
||||
*
|
||||
* @param string $event SSE事件字符串
|
||||
* @return array|null 解析后的事件数据
|
||||
*/
|
||||
protected function parseSseEvent(string $event): ?array
|
||||
{
|
||||
// 提取所有data行的内容
|
||||
if (preg_match_all('/^data: (.+)$/m', $event, $matches)) {
|
||||
// 合并所有data行
|
||||
$data = implode("\n", $matches[1]);
|
||||
return json_decode($data, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理SSE事件输出
|
||||
*
|
||||
* @param string $event SSE事件字符串
|
||||
* @param SymfonyStyle $io SymfonyStyle 实例
|
||||
* @param string|null $pluginName 插件名称(可选)
|
||||
* @return int|null 返回状态码,如果需要提前退出
|
||||
*/
|
||||
protected function handleSseEvent(string $event, SymfonyStyle $io, ?string $pluginName = null): ?int
|
||||
{
|
||||
try {
|
||||
// 直接输出原始事件内容,以便调试
|
||||
if (strpos($event, 'data:') !== false) {
|
||||
// 提取data部分
|
||||
$lines = explode("\n", $event);
|
||||
foreach ($lines as $line) {
|
||||
if (strpos($line, 'data:') === 0) {
|
||||
$data = substr($line, 5);
|
||||
if (!empty(trim($data))) {
|
||||
// 尝试解析JSON
|
||||
$jsonData = json_decode($data, true);
|
||||
if ($jsonData) {
|
||||
$eventType = $jsonData['event'] ?? '';
|
||||
$data = $jsonData['data'] ?? [];
|
||||
|
||||
switch ($eventType) {
|
||||
case 'progress':
|
||||
$progress = $data['progress'] ?? 0;
|
||||
$message = $data['message'] ?? '';
|
||||
$io->text(sprintf("[%d%%] %s", $progress, $message));
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
$message = $data['message'] ?? 'Operation completed successfully';
|
||||
if ($pluginName) {
|
||||
$io->success("Plugin '{$pluginName}' {$message}");
|
||||
} else {
|
||||
$io->success($message);
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
|
||||
case 'error':
|
||||
$io->error($data['message'] ?? 'Operation failed');
|
||||
return Command::FAILURE;
|
||||
|
||||
case 'warning':
|
||||
$io->warning($data['message'] ?? 'Warning');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// 不是JSON,直接输出
|
||||
$io->text(trim($data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不是SSE事件,直接输出
|
||||
$io->text(trim($event));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// 忽略错误,继续执行
|
||||
$io->error('处理SSE事件时出错: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行流式操作(SSE)
|
||||
*
|
||||
* @param iterable $generator 迭代器
|
||||
* @param SymfonyStyle $io SymfonyStyle 实例
|
||||
* @param string|null $pluginName 插件名称
|
||||
* @return int 命令退出码
|
||||
*/
|
||||
protected function executeStream(iterable $generator, SymfonyStyle $io, ?string $pluginName = null): int
|
||||
{
|
||||
foreach ($generator as $event) {
|
||||
$result = $this->handleSseEvent($event, $io, $pluginName);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归删除目录
|
||||
*
|
||||
* @param string $directory 目录路径
|
||||
* @return bool 删除是否成功
|
||||
*/
|
||||
protected function deleteDirectory(string $directory): bool
|
||||
{
|
||||
if (!is_dir($directory)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$files = scandir($directory);
|
||||
foreach ($files as $file) {
|
||||
if ($file === '.' || $file === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $directory . '/' . $file;
|
||||
if (is_dir($path)) {
|
||||
if (!$this->deleteDirectory($path)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!unlink($path)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rmdir($directory);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认操作
|
||||
*
|
||||
* @param SymfonyStyle $io SymfonyStyle 实例
|
||||
* @param string $message 确认消息
|
||||
* @param bool $default 默认值
|
||||
* @return bool 是否确认
|
||||
*/
|
||||
protected function confirm(SymfonyStyle $io, string $message, bool $default = false): bool
|
||||
{
|
||||
return $io->confirm($message, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码输入(带掩码显示)
|
||||
*
|
||||
* @param SymfonyStyle $io SymfonyStyle 实例
|
||||
* @param string $message 提示消息
|
||||
* @param string $mask 掩码字符,默认 *
|
||||
* @return string 输入的密码
|
||||
*/
|
||||
protected function askPassword(SymfonyStyle $io, string $message, string $mask = '*'): string
|
||||
{
|
||||
$io->text($message);
|
||||
|
||||
$password = '';
|
||||
while (true) {
|
||||
$char = $this->readCharacter();
|
||||
|
||||
if ($char === "\n" || $char === "\r") {
|
||||
$io->newLine();
|
||||
break;
|
||||
}
|
||||
|
||||
if ($char === "\x7f" || $char === "\b") { // Backspace
|
||||
if (strlen($password) > 0) {
|
||||
$password = substr($password, 0, -1);
|
||||
$io->write("\b \b");
|
||||
}
|
||||
} else {
|
||||
$password .= $char;
|
||||
$io->write($mask);
|
||||
}
|
||||
}
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取单个字符(支持 Windows/Linux)
|
||||
*/
|
||||
private function readCharacter(): string
|
||||
{
|
||||
// Windows 环境
|
||||
if (DIRECTORY_SEPARATOR === '\\') {
|
||||
return $this->readCharacterWindows();
|
||||
}
|
||||
|
||||
// Linux/Mac 环境
|
||||
$tty = '/dev/tty';
|
||||
if (!is_file($tty) || !is_readable($tty)) {
|
||||
// 回退到标准输入
|
||||
return fgets(STDIN) ?: '';
|
||||
}
|
||||
|
||||
$ttySpec = trim(`stty -g`);
|
||||
system('stty -icanon -echo');
|
||||
$char = fgetc(fopen($tty, 'r'));
|
||||
system("stty {$ttySpec}");
|
||||
|
||||
return $char ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows 环境下读取单个字符
|
||||
*/
|
||||
private function readCharacterWindows(): string
|
||||
{
|
||||
$input = stream_get_contents(STDIN, 1);
|
||||
|
||||
// 处理 Windows 特殊键
|
||||
if ($input === "\xe0" || $input === "\0") {
|
||||
// 功能键前缀,跳过下一个字节
|
||||
stream_get_contents(STDIN, 1);
|
||||
return "\x7f"; // 作为退格键处理
|
||||
}
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认密码输入
|
||||
*
|
||||
* @param SymfonyStyle $io SymfonyStyle 实例
|
||||
* @param string $message 提示消息
|
||||
* @param int $minLength 最小长度
|
||||
* @return string 确认后的密码
|
||||
*/
|
||||
protected function askPasswordWithConfirm(SymfonyStyle $io, string $message, int $minLength = 6): string
|
||||
{
|
||||
$io->text($message);
|
||||
|
||||
while (true) {
|
||||
$password = $this->askPassword($io, '请输入密码: ');
|
||||
|
||||
if (strlen($password) < $minLength) {
|
||||
$io->warning("密码长度不能少于 {$minLength} 位");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 确认密码
|
||||
$confirm = $this->askPassword($io, '请再次输入密码: ');
|
||||
|
||||
if ($password !== $confirm) {
|
||||
$io->error('两次输入的密码不一致,请重新输入');
|
||||
continue;
|
||||
}
|
||||
|
||||
return $password;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出错误信息并记录
|
||||
*
|
||||
* @param SymfonyStyle $io SymfonyStyle 实例
|
||||
* @param string $message 错误消息
|
||||
* @param \Throwable|null $exception 异常对象
|
||||
* @return int 返回失败状态码
|
||||
*/
|
||||
protected function outputError(SymfonyStyle $io, string $message, ?\Throwable $exception = null): int
|
||||
{
|
||||
$io->error($message);
|
||||
if ($exception) {
|
||||
$io->error(sprintf("File: %s Line: %d", $exception->getFile(), $exception->getLine()));
|
||||
if ($io->isVerbose()) {
|
||||
$io->text($exception->getTraceAsString());
|
||||
}
|
||||
}
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出成功信息
|
||||
*
|
||||
* @param SymfonyStyle $io SymfonyStyle 实例
|
||||
* @param string $message 成功消息
|
||||
* @return int 返回成功状态码
|
||||
*/
|
||||
protected function outputSuccess(SymfonyStyle $io, string $message): int
|
||||
{
|
||||
$io->success($message);
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\config;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'madong-config:mysql',
|
||||
description: '显示当前MySQL服务器配置',
|
||||
aliases: ['madong-config:mysql']
|
||||
)]
|
||||
class ConfigMySQLCommand extends BaseCommand
|
||||
{
|
||||
public function __invoke(OutputInterface $output): int
|
||||
{
|
||||
$output->writeln('MySQL配置信息如下:');
|
||||
$config = config('database');
|
||||
$headers = ['name', 'default', 'driver', 'host', 'port', 'database', 'username', 'password', 'unix_socket', 'charset', 'collation', 'prefix', 'strict', 'engine', 'schema', 'sslmode'];
|
||||
$rows = [];
|
||||
foreach ($config['connections'] as $name => $db_config) {
|
||||
$row = [];
|
||||
foreach ($headers as $key) {
|
||||
switch ($key) {
|
||||
case 'name':
|
||||
$row[] = $name;
|
||||
break;
|
||||
case 'default':
|
||||
$row[] = $config['default'] == $name ? 'true' : 'false';
|
||||
break;
|
||||
default:
|
||||
$row[] = $db_config[$key] ?? '';
|
||||
}
|
||||
}
|
||||
if ($config['default'] == $name) {
|
||||
array_unshift($rows, $row);
|
||||
} else {
|
||||
$rows[] = $row;
|
||||
}
|
||||
}
|
||||
$table = new Table($output);
|
||||
$table->setHeaders($headers);
|
||||
$table->setRows($rows);
|
||||
$table->render();
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\install;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 下载前端代码命令
|
||||
* 使用方法:
|
||||
* - 下载所有前端代码:php webman madong-download:frontend
|
||||
* - 下载指定代码:php webman madong-download:frontend --admin --web
|
||||
* - 指定分支:php webman madong-download:frontend -b develop
|
||||
* - 强制更新:php webman madong-download:frontend -f
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-download:frontend',
|
||||
description: 'Download frontend code to the root directory',
|
||||
aliases: ['madong-download:frontend'],
|
||||
hidden: false
|
||||
)]
|
||||
class DownloadCommand extends BaseCommand
|
||||
{
|
||||
// 前端配置信息 - 支持灵活配置下载项目和重命名
|
||||
// 所有前端代码统一下载到 frontend 目录下
|
||||
private array $frontendConfigs = [
|
||||
'admin' => [
|
||||
'name' => '后台下载',
|
||||
'git_url' => 'https://gitee.com/motion-code/madong-vue.git',
|
||||
'dir_name' => 'frontend' . DIRECTORY_SEPARATOR . 'admin',
|
||||
],
|
||||
'web' => [
|
||||
'name' => '前台下载',
|
||||
'git_url' => 'https://gitee.com/motion-code/madong-nuxt.git',
|
||||
'dir_name' => 'frontend' . DIRECTORY_SEPARATOR . 'web',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('admin', 'a', InputOption::VALUE_NONE, 'Download admin frontend code')
|
||||
->addOption('web', 'w', InputOption::VALUE_NONE, 'Download web frontend code')
|
||||
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force download (overwrite existing files)')
|
||||
->addOption('branch', 'b', InputOption::VALUE_REQUIRED, 'Specify git branch to download', 'main');
|
||||
|
||||
// 为配置的每个项目自动添加选项
|
||||
foreach (array_keys($this->frontendConfigs) as $project) {
|
||||
if (!in_array($project, ['admin', 'web'])) { // 跳过已经手动添加的选项
|
||||
$this->addOption($project, null, InputOption::VALUE_NONE, "Download {$project} frontend code");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Frontend Code Downloader');
|
||||
|
||||
// 验证Git是否可用
|
||||
if (!$this->isGitAvailable()) {
|
||||
$io->error('Git is not available on your system. Please install Git first.');
|
||||
$io->note('You can download Git from https://git-scm.com/downloads');
|
||||
return $this->outputError($io, 'Git is not available');
|
||||
}
|
||||
|
||||
// 获取参数
|
||||
$forceDownload = $input->getOption('force');
|
||||
$branch = $input->getOption('branch');
|
||||
|
||||
// 确定要下载的项目列表
|
||||
$projectsToDownload = [];
|
||||
$anyOptionSelected = false;
|
||||
|
||||
foreach (array_keys($this->frontendConfigs) as $project) {
|
||||
if ($input->getOption($project)) {
|
||||
$projectsToDownload[] = $project;
|
||||
$anyOptionSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有指定具体项目,默认下载所有
|
||||
if (!$anyOptionSelected) {
|
||||
$projectsToDownload = array_keys($this->frontendConfigs);
|
||||
}
|
||||
|
||||
// 获取当前项目根目录
|
||||
$rootDir = dirname(base_path());
|
||||
$io->info("Project root directory: {$rootDir}");
|
||||
|
||||
// 确保 frontend 目录存在
|
||||
$frontendDir = $rootDir . DIRECTORY_SEPARATOR . 'frontend';
|
||||
if (!is_dir($frontendDir)) {
|
||||
if (!mkdir($frontendDir, 0755, true)) {
|
||||
$io->error("Failed to create frontend directory");
|
||||
return $this->outputError($io, 'Failed to create frontend directory');
|
||||
}
|
||||
$io->info("Created frontend directory");
|
||||
}
|
||||
|
||||
$io->info("Using git mode for downloading code");
|
||||
|
||||
// 初始化统计信息
|
||||
$successCount = 0;
|
||||
$totalCount = count($projectsToDownload);
|
||||
|
||||
// 逐个下载前端项目
|
||||
foreach ($projectsToDownload as $project) {
|
||||
$frontendConfig = $this->frontendConfigs[$project];
|
||||
$io->section("Downloading: {$frontendConfig['name']}");
|
||||
|
||||
// 定义前端代码保存的目录和git地址
|
||||
$frontendGitUrl = $frontendConfig['git_url'];
|
||||
$frontendDir = $frontendConfig['dir_name'];
|
||||
$frontendFullPath = $rootDir . DIRECTORY_SEPARATOR . $frontendDir;
|
||||
|
||||
// 检查前端代码目录是否已存在
|
||||
if (is_dir($frontendFullPath)) {
|
||||
if (!$forceDownload) {
|
||||
if (!$io->confirm("Directory '{$frontendDir}' already exists. Do you want to update it?", true)) {
|
||||
$io->note("Skipping update for {$frontendDir} directory");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$io->info("Directory exists, updating {$frontendDir} code...");
|
||||
|
||||
// 执行git pull命令
|
||||
$command = "cd \"{$frontendFullPath}\" && git pull origin {$branch}";
|
||||
$io->text("Executing: {$command}");
|
||||
|
||||
$outputBuffer = [];
|
||||
$returnValue = 0;
|
||||
exec($command, $outputBuffer, $returnValue);
|
||||
|
||||
// 输出结果
|
||||
foreach ($outputBuffer as $line) {
|
||||
$io->text($line);
|
||||
}
|
||||
|
||||
if ($returnValue !== 0) {
|
||||
$io->error("Failed to update {$frontendDir}");
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$io->info("Directory does not exist, cloning {$frontendDir} code...");
|
||||
|
||||
// 执行git clone命令 - 自动重命名为配置的目录名
|
||||
$command = "git clone -b {$branch} {$frontendGitUrl} {$frontendDir}";
|
||||
$io->text("Executing: {$command}");
|
||||
|
||||
$outputBuffer = [];
|
||||
$returnValue = 0;
|
||||
exec("cd \"{$rootDir}\" && " . $command, $outputBuffer, $returnValue);
|
||||
|
||||
// 输出结果
|
||||
foreach ($outputBuffer as $line) {
|
||||
$io->text($line);
|
||||
}
|
||||
|
||||
if ($returnValue !== 0) {
|
||||
$io->error("Failed to clone {$frontendDir}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$io->success("Successfully downloaded {$frontendDir} frontend code");
|
||||
$io->note("Saved to: {$frontendFullPath}");
|
||||
$successCount++;
|
||||
}
|
||||
|
||||
// 输出总结
|
||||
$io->section('Download Summary');
|
||||
if ($successCount === $totalCount) {
|
||||
$io->success("Successfully downloaded all {$successCount} frontend code!");
|
||||
return Command::SUCCESS;
|
||||
} elseif ($successCount > 0) {
|
||||
$io->warning("Downloaded {$successCount} of {$totalCount} frontend code successfully");
|
||||
return Command::FAILURE;
|
||||
} else {
|
||||
$io->error("Failed to download any frontend code");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Git是否可用
|
||||
*/
|
||||
private function isGitAvailable(): bool
|
||||
{
|
||||
$output = [];
|
||||
$returnValue = 0;
|
||||
exec('git --version', $output, $returnValue);
|
||||
return $returnValue === 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\install;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use app\service\core\install\InstallService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 框架安装命令
|
||||
*
|
||||
* 支持 CLI 交互式安装和在线安装共享同一套服务逻辑
|
||||
*
|
||||
* 使用示例:
|
||||
* php webman install:madong
|
||||
* php webman install:madong --username=admin --password=123456
|
||||
* php webman install:madong --db-host=localhost --db-port=3306 --db-name=madong --db-user=root --db-pass=123456 --admin-username=admin --admin-password=123456
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'install:madong',
|
||||
description: '安装 MDAdmin 框架(交互式或参数化)',
|
||||
aliases: ['install:madong'],
|
||||
hidden: false
|
||||
)]
|
||||
class InstallCommand extends BaseCommand
|
||||
{
|
||||
|
||||
private ?InstallService $installService = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
// 不在构造函数中实例化 InstallService,避免命令加载时触发依赖初始化
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 InstallService 实例(延迟加载)
|
||||
*/
|
||||
private function getInstallService(): InstallService
|
||||
{
|
||||
if ($this->installService === null) {
|
||||
try {
|
||||
$this->installService = new InstallService();
|
||||
} catch (\Throwable $e) {
|
||||
throw new \RuntimeException(
|
||||
'Failed to initialize InstallService: ' . $e->getMessage(),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
return $this->installService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置命令参数和选项
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('安装 MDAdmin 框架')
|
||||
->setHelp('此命令用于安装 MDAdmin 框架,支持交互式和参数化两种方式')
|
||||
|
||||
// 数据库参数
|
||||
->addOption('db-host', null, InputOption::VALUE_OPTIONAL, '数据库主机')
|
||||
->addOption('db-port', null, InputOption::VALUE_OPTIONAL, '数据库端口', '3306')
|
||||
->addOption('db-name', null, InputOption::VALUE_OPTIONAL, '数据库名称')
|
||||
->addOption('db-user', null, InputOption::VALUE_OPTIONAL, '数据库用户名')
|
||||
->addOption('db-pass', null, InputOption::VALUE_OPTIONAL, '数据库密码')
|
||||
->addOption('db-prefix', null, InputOption::VALUE_OPTIONAL, '数据库表前缀', 'ma_')
|
||||
|
||||
// 管理员参数
|
||||
->addOption('admin-username', null, InputOption::VALUE_OPTIONAL, '管理员用户名')
|
||||
->addOption('admin-password', null, InputOption::VALUE_OPTIONAL, '管理员密码')
|
||||
->addOption('admin-email', null, InputOption::VALUE_OPTIONAL, '管理员邮箱')
|
||||
|
||||
// 安装选项
|
||||
->addOption('install-database', null, InputOption::VALUE_OPTIONAL, '是否安装数据库表(1=是,0=否)', '1')
|
||||
->addOption('build-project', null, InputOption::VALUE_OPTIONAL, '是否构建前端项目(1=是,0=否)', '1')
|
||||
->addOption('non-interactive', 'y', InputOption::VALUE_NONE, '非交互模式(使用所有默认值)')
|
||||
->addOption('skip-env-check', null, InputOption::VALUE_NONE, '跳过环境检查');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行安装命令
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
// 显示欢迎信息
|
||||
$io->title('MDAdmin 框架安装程序');
|
||||
$io->text([
|
||||
'',
|
||||
'欢迎使用 MDAdmin 框架安装程序!',
|
||||
'此程序将引导您完成框架的安装配置。',
|
||||
'',
|
||||
'<comment>提示: 密码输入时不会显示字符(出于安全考虑),输入完成后会显示确认信息</comment>',
|
||||
'',
|
||||
]);
|
||||
|
||||
// 检查是否已安装
|
||||
if ($this->getInstallService()->checkInstalled()) {
|
||||
$io->error('系统已安装,请勿重复安装!');
|
||||
$io->note('如需重新安装,请先删除 ' . base_path('install.lock') . ' 文件');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// 交互式或非交互式
|
||||
$isInteractive = !$input->getOption('non-interactive');
|
||||
$skipEnvCheck = $input->getOption('skip-env-check');
|
||||
|
||||
// 1. 环境检测
|
||||
if (!$skipEnvCheck) {
|
||||
$io->section('1. 环境检测');
|
||||
$this->handleEnvironmentCheck($io);
|
||||
}
|
||||
|
||||
// 2. 数据库配置
|
||||
$io->section('2. 数据库配置');
|
||||
$installParams = $this->collectInstallParams($io, $input, $isInteractive);
|
||||
|
||||
// 3. 确认安装
|
||||
$io->section('3. 安装确认');
|
||||
if (!$this->confirmInstallation($io, $installParams, $isInteractive)) {
|
||||
$io->note('安装已取消');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// 4. 执行安装
|
||||
$io->section('4. 执行安装');
|
||||
$io->text('开始安装...');
|
||||
|
||||
try {
|
||||
// 调用安装服务(共享在线安装逻辑)
|
||||
$generator = $this->getInstallService()->install($installParams);
|
||||
|
||||
// 处理 SSE 事件输出
|
||||
foreach ($generator as $chunk) {
|
||||
$result = $this->handleSseEvent($chunk, $io, null);
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Throwable $e) {
|
||||
$this->outputError($io, '安装失败: ' . $e->getMessage(), $e);
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理环境检查
|
||||
*/
|
||||
private function handleEnvironmentCheck(SymfonyStyle $io): void
|
||||
{
|
||||
$environment = $this->getInstallService()->checkEnvironment();
|
||||
|
||||
// 显示系统信息
|
||||
$io->text('系统信息:');
|
||||
$io->definitionList(
|
||||
['PHP 版本' => $environment['php_version']],
|
||||
['服务器软件' => $environment['server_software']],
|
||||
['操作系统' => $environment['operating_system']],
|
||||
['内存限制' => $environment['memory_limit']],
|
||||
['最大执行时间' => $environment['max_execution_time'] . '秒'],
|
||||
);
|
||||
$io->newLine();
|
||||
|
||||
// 检查系统要求
|
||||
$allOk = $environment['all_requirements_met'];
|
||||
|
||||
if (!$allOk) {
|
||||
$io->warning('环境检查未通过,请检查以下项目:');
|
||||
|
||||
// 显示失败的检查项
|
||||
foreach ($environment['check_items'] as $item) {
|
||||
if (!$item['status']) {
|
||||
$io->error($item['message']);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($environment['directory_check_items'] as $item) {
|
||||
if (!$item['status'] && $item['required']) {
|
||||
$io->error($item['message']);
|
||||
}
|
||||
}
|
||||
|
||||
// 确认是否继续
|
||||
if (!$this->confirm($io, '环境检查未通过,是否继续安装?', false)) {
|
||||
throw new \Exception('环境检查未通过,安装已取消');
|
||||
}
|
||||
} else {
|
||||
$io->success('环境检查通过!');
|
||||
}
|
||||
|
||||
$io->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集安装参数
|
||||
*/
|
||||
private function collectInstallParams(SymfonyStyle $io, InputInterface $input, bool $isInteractive): array
|
||||
{
|
||||
$params = [];
|
||||
|
||||
// 数据库配置
|
||||
$io->text('<info>数据库配置</info>');
|
||||
|
||||
$dbHost = $input->getOption('db-host');
|
||||
if (!$dbHost && $isInteractive) {
|
||||
$dbHost = $io->ask('请输入数据库主机', '127.0.0.1');
|
||||
}
|
||||
$params['db_host'] = $dbHost ?? '127.0.0.1';
|
||||
|
||||
$dbPort = $input->getOption('db-port');
|
||||
if (!$dbPort && $isInteractive) {
|
||||
$dbPort = $io->ask('请输入数据库端口', '3306');
|
||||
}
|
||||
$params['db_port'] = $dbPort ?? '3306';
|
||||
|
||||
$dbName = $input->getOption('db-name');
|
||||
if (!$dbName && $isInteractive) {
|
||||
$dbName = $io->ask('请输入数据库名称', 'md_admin');
|
||||
}
|
||||
$params['db_database'] = $dbName ?? 'md_admin';
|
||||
|
||||
$dbUser = $input->getOption('db-user');
|
||||
if (!$dbUser && $isInteractive) {
|
||||
$dbUser = $io->ask('请输入数据库用户名', 'root');
|
||||
}
|
||||
$params['db_username'] = $dbUser ?? 'root';
|
||||
|
||||
$dbPass = $input->getOption('db-pass');
|
||||
if (!$dbPass && $isInteractive) {
|
||||
$io->text('提示: 密码输入时显示掩码字符 *');
|
||||
$dbPass = $this->askPassword($io, '请输入数据库密码: ');
|
||||
if ($dbPass !== '') {
|
||||
$io->text('✓ 数据库密码已设置');
|
||||
}
|
||||
}
|
||||
$params['db_password'] = $dbPass ?? '';
|
||||
|
||||
$dbPrefix = $input->getOption('db-prefix');
|
||||
if (!$dbPrefix && $isInteractive) {
|
||||
$dbPrefix = $io->ask('请输入数据库表前缀', 'ma_');
|
||||
}
|
||||
$params['db_prefix'] = $dbPrefix ?? 'ma_';
|
||||
|
||||
// 管理员配置
|
||||
$io->newLine();
|
||||
$io->text('<info>管理员配置</info>');
|
||||
|
||||
$adminUsername = $input->getOption('admin-username');
|
||||
if (!$adminUsername && $isInteractive) {
|
||||
$adminUsername = $io->ask('请输入管理员用户名', 'admin');
|
||||
}
|
||||
$params['admin_username'] = $adminUsername ?? 'admin';
|
||||
|
||||
$adminPassword = $input->getOption('admin-password');
|
||||
if (!$adminPassword && $isInteractive) {
|
||||
$io->text('提示: 密码输入时显示掩码字符 *');
|
||||
do {
|
||||
$adminPassword = $this->askPassword($io, '请输入管理员密码(至少6位): ');
|
||||
if (strlen($adminPassword) < 6) {
|
||||
$io->error('密码长度不能少于6位!');
|
||||
}
|
||||
} while (strlen($adminPassword) < 6);
|
||||
$io->text('✓ 管理员密码已设置');
|
||||
}
|
||||
$params['admin_password'] = $adminPassword ?? '123456';
|
||||
|
||||
$adminEmail = $input->getOption('admin-email');
|
||||
if (!$adminEmail && $isInteractive) {
|
||||
$adminEmail = $io->ask('请输入管理员邮箱', 'admin@example.com');
|
||||
}
|
||||
$params['admin_email'] = $adminEmail ?? 'admin@example.com';
|
||||
|
||||
// 安装选项
|
||||
$io->newLine();
|
||||
$io->text('<info>安装选项</info>');
|
||||
|
||||
$installDatabase = $input->getOption('install-database');
|
||||
if ($installDatabase === null && $isInteractive) {
|
||||
$installDatabase = $this->confirm($io, '是否安装数据库表?', true) ? '1' : '0';
|
||||
}
|
||||
$params['install_database'] = $installDatabase ?? '1';
|
||||
|
||||
$buildProject = $input->getOption('build-project');
|
||||
if ($buildProject === null && $isInteractive) {
|
||||
$buildProject = $this->confirm($io, '是否构建前端项目?', false) ? '1' : '0';
|
||||
}
|
||||
$params['build_project'] = $buildProject ?? '0';
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认安装信息
|
||||
*/
|
||||
private function confirmInstallation(SymfonyStyle $io, array $params, bool $isInteractive): bool
|
||||
{
|
||||
// 显示配置摘要
|
||||
$io->text('安装配置摘要:');
|
||||
$io->definitionList(
|
||||
['数据库主机' => $params['db_host']],
|
||||
['数据库端口' => $params['db_port']],
|
||||
['数据库名称' => $params['db_database']],
|
||||
['数据库用户' => $params['db_username']],
|
||||
['表前缀' => $params['db_prefix']],
|
||||
['管理员用户名' => $params['admin_username']],
|
||||
['管理员邮箱' => $params['admin_email']],
|
||||
['安装数据库' => $params['install_database'] == '1' ? '是' : '否'],
|
||||
['构建前端' => $params['build_project'] == '1' ? '是' : '否'],
|
||||
);
|
||||
$io->newLine();
|
||||
|
||||
// 确认安装
|
||||
if ($isInteractive) {
|
||||
return $this->confirm($io, '确认开始安装?安装前请确保数据库配置正确!', false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,798 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
namespace app\command\install;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use support\Db;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
//创建迁移(自动表名) madong-migrate make RoleBelongsMenu 生成小写蛇形文件名,提示表名
|
||||
//创建迁移(手动表名) madong-migrate make RoleBelongsMenu --table=user_roles 使用指定表名,生成对应文件名
|
||||
//创建种子 madong-migrate make-seed User 生成UserSeeder.php
|
||||
//执行迁移 madong-migrate up 输出已迁移的文件名
|
||||
//迁移+种子 madong-migrate up --seed 先迁移,后运行种子
|
||||
//回滚迁移 madong-migrate rollback 输出回滚的文件名,清理日志
|
||||
//运行种子 madong-migrate seed 输出已运行的种子类名
|
||||
//查看状态 madong-migrate status 显示迁移状态
|
||||
//批量回滚 madong-migrate rollback --batch=2 回滚到指定批次
|
||||
//干运行模式 madong-migrate up --dry-run 预览将要执行的迁移(不实际执行)
|
||||
//导出SQL(含数据) madong-migrate up --sql=install.sql 迁移+种子+导出SQL(含表结构和数据)
|
||||
//导出SQL(无数据) madong-migrate up --sql=install.sql --no-data 只导出表结构,不包含数据
|
||||
|
||||
/**
|
||||
* 数据迁移
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0
|
||||
*/
|
||||
class MigrateCommand extends BaseCommand
|
||||
{
|
||||
protected static string $defaultName = 'madong-migrate';
|
||||
protected static string $defaultDescription = '数据迁移和种子数据 (迁移+种子+导出SQL)';
|
||||
protected ?string $connection = null;
|
||||
protected bool $dryRun = false;
|
||||
protected bool $exportSql = false;
|
||||
protected bool $exportWithData = true;
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument(
|
||||
'operate',
|
||||
InputArgument::OPTIONAL,
|
||||
'Operation: make/make-seed/up/rollback/seed/status',
|
||||
'up',
|
||||
['make', 'make-seed', 'up', 'rollback', 'seed', 'status']
|
||||
);
|
||||
$this->addArgument('name', InputArgument::OPTIONAL, 'Migration/Seeder name (camelCase)');
|
||||
$this->addOption('connection', null, InputOption::VALUE_OPTIONAL, 'DB connection [default: "default"]');
|
||||
$this->addOption('seed', null, InputOption::VALUE_NONE, 'Run seeders after migration');
|
||||
$this->addOption('table', 't', InputOption::VALUE_OPTIONAL, 'Specify table name for migration (auto-generate if not provided)');
|
||||
$this->addOption('batch', 'b', InputOption::VALUE_OPTIONAL, 'Batch number to rollback to (for rollback operation)');
|
||||
$this->addOption('dry-run', 'd', InputOption::VALUE_NONE, 'Preview migrations without executing (for up operation)');
|
||||
$this->addOption('sql', 's', InputOption::VALUE_OPTIONAL, 'Export SQL to file (包含表结构和种子数据), e.g: --sql=install.sql');
|
||||
$this->addOption('no-data', null, InputOption::VALUE_NONE, 'Export SQL without data (only table structure)');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$operate = strtolower($input->getArgument('operate'));
|
||||
$name = $input->getArgument('name');
|
||||
$this->connection = $input->getOption('connection');
|
||||
$runSeed = $input->getOption('seed');
|
||||
$this->dryRun = $input->getOption('dry-run');
|
||||
$sqlOption = $input->getOption('sql');
|
||||
$this->exportWithData = !$input->getOption('no-data');
|
||||
// 检测是否使用 --sql 选项(必须指定文件名)
|
||||
$this->exportSql = is_string($sqlOption) && strlen(trim($sqlOption)) > 0;
|
||||
|
||||
$validOperations = ['make', 'make-seed', 'up', 'rollback', 'seed', 'status'];
|
||||
if (!in_array($operate, $validOperations)) {
|
||||
$output->writeln("<error>❌ Invalid operation: {$operate}</error>");
|
||||
return Command::INVALID;
|
||||
}
|
||||
|
||||
if ($this->dryRun && !in_array($operate, ['up', 'rollback'])) {
|
||||
$output->writeln("<error>❌ --dry-run is only supported for 'up' and 'rollback' operations</error>");
|
||||
return Command::INVALID;
|
||||
}
|
||||
|
||||
if ($this->exportSql && !in_array($operate, ['up', 'rollback'])) {
|
||||
$output->writeln("<error>❌ --sql is only supported for 'up' and 'rollback' operations</error>");
|
||||
return Command::INVALID;
|
||||
}
|
||||
|
||||
if ($this->dryRun) {
|
||||
$output->writeln("<comment>🔍 DRY RUN MODE - No changes will be made</comment>");
|
||||
}
|
||||
|
||||
if ($this->exportSql) {
|
||||
$output->writeln("<comment>📄 SQL EXPORT MODE - SQL will be saved to file</comment>");
|
||||
}
|
||||
|
||||
match ($operate) {
|
||||
'make' => $this->runMakeMigration($name, $input, $output),
|
||||
'make-seed' => $this->runMakeSeeder($name, $output),
|
||||
'up' => $this->runMigrations($output, $runSeed, $sqlOption, $name),
|
||||
'rollback' => $this->runRollback($output, $input->getOption('batch'), $sqlOption),
|
||||
'seed' => $this->runSeeders($output),
|
||||
'status' => $this->runStatus($output),
|
||||
};
|
||||
|
||||
if (!$this->dryRun) {
|
||||
$output->writeln('<info>✅ Operation completed successfully!</info>');
|
||||
}
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰转下划线(小写)
|
||||
*/
|
||||
protected function camelToSnake(string $str): string
|
||||
{
|
||||
return strtolower(preg_replace('/(?<!^)([A-Z])/', '_$1', $str));
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰(首字母大写)
|
||||
*/
|
||||
protected function snakeToCamel(string $str): string
|
||||
{
|
||||
return str_replace('_', '', ucwords($str, '_'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成表名(驼峰转下划线 + 复数)
|
||||
*/
|
||||
protected function generateTableName(string $name): string
|
||||
{
|
||||
return $this->camelToSnake($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建迁移文件(修复多余下划线问题)
|
||||
*/
|
||||
protected function runMakeMigration(string $name, InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$migrationDir = base_path('resource/database/migrations');
|
||||
if (!is_dir($migrationDir)) {
|
||||
mkdir($migrationDir, 0777, true);
|
||||
}
|
||||
|
||||
// 1. 获取表名(手动指定或自动生成)
|
||||
$specifiedTableName = $input->getOption('table');
|
||||
$tableName = $specifiedTableName ?? $this->generateTableName($name);
|
||||
|
||||
// 提示表名来源
|
||||
if ($specifiedTableName) {
|
||||
$output->writeln("<info>ℹ️ Using specified table: {$tableName}</info>");
|
||||
} else {
|
||||
$output->writeln("<info>ℹ️ Generated table: {$tableName}</info>");
|
||||
}
|
||||
|
||||
// 2. 生成「文件名前缀」(纯驼峰格式:create + 表名驼峰)
|
||||
$tableCamel = $this->snakeToCamel($tableName); // 如 role_belongs_menus → RoleBelongsMenus
|
||||
$filenamePrefix = 'create' . $tableCamel; // 如 createRoleBelongsMenus(关键修正:去掉下划线)
|
||||
|
||||
// 3. 转换为小写蛇形(最终文件名核心:create_role_belongs_menus)
|
||||
$snakeFilenamePrefix = $this->camelToSnake($filenamePrefix);
|
||||
|
||||
// 4. 生成完整文件名(时间戳 + 小写蛇形前缀 + .php)
|
||||
$fileName = date('Y_m_d_His_') . $snakeFilenamePrefix . '.php';
|
||||
$filePath = "{$migrationDir}/{$fileName}";
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
$output->writeln("<error>❌ Migration file exists: {$fileName}</error>");
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 生成迁移模板(匿名类,文件名小写蛇形)
|
||||
$template = $this->generateMigrationTemplate($tableName);
|
||||
file_put_contents($filePath, $template);
|
||||
$output->writeln("<info>📝 Migration created: {$fileName}</info>");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成迁移模板(保持不变)
|
||||
*/
|
||||
protected function generateMigrationTemplate(string $tableName): string
|
||||
{
|
||||
return <<<EOF
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use support\Db;
|
||||
|
||||
return new class {
|
||||
|
||||
public function up(Builder \$schema): void
|
||||
{
|
||||
// 创建数据表(表名:{$tableName})
|
||||
\$schema->create('{$tableName}', function(Blueprint \$table) {
|
||||
\$table->bigInteger('id')->primary()->comment('主键');
|
||||
\$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(Builder \$schema): void
|
||||
{
|
||||
// 回滚:删除表(表名:{$tableName})
|
||||
\$schema->dropIfExists('{$tableName}');
|
||||
}
|
||||
};
|
||||
EOF;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建种子
|
||||
*
|
||||
* @param string $name
|
||||
* @param \Symfony\Component\Console\Output\OutputInterface $output
|
||||
*/
|
||||
protected function runMakeSeeder(string $name, OutputInterface $output)
|
||||
{
|
||||
$seederClassPrefix = $this->snakeToCamel($name);
|
||||
|
||||
$seederDir = base_path('resource/database/seeds');
|
||||
if (!is_dir($seederDir)) {
|
||||
mkdir($seederDir, 0777, true);
|
||||
}
|
||||
|
||||
// 🔑 文件名用驼峰前缀 + Seeder.php(确保与类名一致)
|
||||
$fileName = "{$seederClassPrefix}Seeder.php";
|
||||
$filePath = "{$seederDir}/{$fileName}";
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
$output->writeln("<error>❌ Seeder exists: {$fileName}</error>");
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔑 传入驼峰前缀生成模板(类名直接拼接前缀)
|
||||
$template = $this->generateSeederTemplate($seederClassPrefix);
|
||||
file_put_contents($filePath, $template);
|
||||
|
||||
// ✅ 明确提示生成的文件和类名
|
||||
$output->writeln("<info>📝 Seeder created: {$fileName}</info>");
|
||||
$output->writeln("<comment>ℹ️ Seeder class: {$seederClassPrefix}Seeder</comment>");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成种子模板
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateSeederTemplate(string $name): string
|
||||
{
|
||||
$className = ucfirst($name) . 'Seeder';
|
||||
return <<<EOF
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use support\Db;
|
||||
|
||||
class {$className}
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
// 示例:插入用户数据
|
||||
/*
|
||||
Db::table('users')->insert([
|
||||
'username' => 'seed_user',
|
||||
'email' => 'seed@example.com',
|
||||
'password' => password_hash('secure', PASSWORD_DEFAULT),
|
||||
]);
|
||||
*/
|
||||
}
|
||||
}
|
||||
EOF;
|
||||
}
|
||||
|
||||
protected function runSeeders(OutputInterface $output)
|
||||
{
|
||||
$seederDir = base_path('resource/database/seeds');
|
||||
if (!is_dir($seederDir)) {
|
||||
$output->writeln("<comment>ℹ️ No seeders directory</comment>");
|
||||
return;
|
||||
}
|
||||
|
||||
$files = glob("{$seederDir}/*.php");
|
||||
if (empty($files)) {
|
||||
$output->writeln("<comment>ℹ️ No seeders available</comment>");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
// 拼接完整命名空间(resource\database\seeds\文件名)
|
||||
$fileNameWithoutExt = pathinfo($file, PATHINFO_FILENAME);
|
||||
$fullClassName = 'resource\\database\\seeds\\' . $fileNameWithoutExt; // 完整类名:resource\database\seeds\AdminSeeder
|
||||
|
||||
try {
|
||||
// 1. 加载种子文件(注册类到自动加载)
|
||||
require_once $file; // 用require_once避免重复加载
|
||||
|
||||
// 2. 检查类是否已正确定义(用完整类名)
|
||||
if (!class_exists($fullClassName)) {
|
||||
throw new \InvalidArgumentException("Seeder class '{$fullClassName}' not found in file '{$file}'");
|
||||
}
|
||||
|
||||
// 3. 实例化种子类(用完整类名)
|
||||
$seederInstance = new $fullClassName();
|
||||
|
||||
// 4. 执行run方法
|
||||
$seederInstance->run();
|
||||
|
||||
$output->writeln("<info>✅ Seeded: {$fullClassName}</info>");
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln("<error>❌ Error seeding {$fullClassName}: {$e->getMessage()}</error>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看迁移状态
|
||||
*/
|
||||
protected function runStatus(OutputInterface $output)
|
||||
{
|
||||
$logFile = $this->getMigrationLogFile();
|
||||
$migrationDir = base_path('resource/database/migrations');
|
||||
|
||||
if (!is_dir($migrationDir)) {
|
||||
$output->writeln("<comment>ℹ️ No migrations directory found</comment>");
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = $this->fetchMigrationRows($logFile);
|
||||
$existingMigrations = array_column($rows, 1);
|
||||
|
||||
// 获取所有迁移文件
|
||||
$files = glob("{$migrationDir}/*.php");
|
||||
$allMigrations = [];
|
||||
foreach ($files as $file) {
|
||||
$basename = basename($file, '.php');
|
||||
$allMigrations[] = [
|
||||
'filename' => $basename,
|
||||
'status' => in_array($basename, $existingMigrations) ? '✅ Migrated' : '⏳ Pending'
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($allMigrations)) {
|
||||
$output->writeln("<comment>ℹ️ No migration files found</comment>");
|
||||
return;
|
||||
}
|
||||
|
||||
// 按文件名排序
|
||||
sort($allMigrations);
|
||||
|
||||
// 按批次分组显示
|
||||
$batches = [];
|
||||
foreach ($rows as $row) {
|
||||
$batches[$row[0]][] = $row[1];
|
||||
}
|
||||
|
||||
$output->writeln("<info>📊 Migration Status</info>");
|
||||
$output->writeln(str_repeat('=', 50));
|
||||
|
||||
$totalMigrated = count($existingMigrations);
|
||||
$totalPending = count($allMigrations) - $totalMigrated;
|
||||
|
||||
$output->writeln("<info>Migrated: {$totalMigrated} | Pending: {$totalPending} | Total: " . count($allMigrations) . "</info>");
|
||||
$output->writeln(str_repeat('-', 50));
|
||||
|
||||
// 显示所有迁移及其状态
|
||||
foreach ($allMigrations as $migration) {
|
||||
if ($migration['status'] === '✅ Migrated') {
|
||||
$output->writeln("<info>{$migration['status']}: {$migration['filename']}</info>");
|
||||
} else {
|
||||
$output->writeln("<comment>{$migration['status']}: {$migration['filename']}</comment>");
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($batches)) {
|
||||
$output->writeln(str_repeat('-', 50));
|
||||
$output->writeln("<info>📦 Migration Batches</info>");
|
||||
krsort($batches);
|
||||
foreach ($batches as $batch => $migrations) {
|
||||
$output->writeln("<comment>Batch {$batch}:</comment> " . implode(', ', $migrations));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移
|
||||
*
|
||||
* @param \Symfony\Component\Console\Output\OutputInterface $output
|
||||
* @param bool $runSeed
|
||||
* @param string|bool $sqlOption
|
||||
*/
|
||||
protected function runMigrations(OutputInterface $output, bool $runSeed = false, $sqlOption = false, ?string $migrationName = null)
|
||||
{
|
||||
$logFile = $this->getMigrationLogFile();
|
||||
$rows = $this->fetchMigrationRows($logFile);
|
||||
$latestBatch = empty($rows) ? 0 : (int)end($rows)[0];
|
||||
$existingMigrations = array_column($rows, 1);
|
||||
$newMigrations = [];
|
||||
|
||||
|
||||
$migrationDir = base_path('resource/database/migrations');
|
||||
|
||||
if (!is_dir($migrationDir)) {
|
||||
$output->writeln("<comment>ℹ️ No migrations directory found</comment>");
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 glob 替代 DirectoryIterator 提高性能
|
||||
$files = glob("{$migrationDir}/*.php");
|
||||
|
||||
foreach ($files as $file) {
|
||||
$basename = basename($file, '.php');
|
||||
if ($migrationName !== null && $migrationName !== '' && $basename !== pathinfo($migrationName, PATHINFO_FILENAME)) {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($basename, $existingMigrations)) {
|
||||
$newMigrations[] = [$file, $basename];
|
||||
}
|
||||
}
|
||||
|
||||
if ($migrationName !== null && $migrationName !== '' && empty($newMigrations)) {
|
||||
$targetName = pathinfo($migrationName, PATHINFO_FILENAME);
|
||||
if (in_array($targetName, $existingMigrations)) {
|
||||
$output->writeln("<comment>ℹ️ Migration already executed: {$targetName}</comment>");
|
||||
} else {
|
||||
$output->writeln("<error>❌ Migration file not found: {$targetName}</error>");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($newMigrations)) {
|
||||
$output->writeln("<comment>ℹ️ No pending migrations</comment>");
|
||||
if ($runSeed) {
|
||||
$this->runSeeders($output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sort($newMigrations);
|
||||
$schema = $this->getSchemaBuilderWithInnoDB();
|
||||
|
||||
if ($this->dryRun) {
|
||||
$output->writeln("<comment>🔍 Pending migrations to execute:</comment>");
|
||||
foreach ($newMigrations as $migration) {
|
||||
$output->writeln(" - {$migration[1]}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是导出 SQL 模式
|
||||
if ($this->exportSql) {
|
||||
$this->exportMigrationsToSql($output, $newMigrations, $sqlOption);
|
||||
return;
|
||||
}
|
||||
|
||||
$batchNum = $latestBatch + 1;
|
||||
$capturedSql = [];
|
||||
|
||||
// 启用查询监听以捕获 SQL
|
||||
Db::connection($this->connection)->listen(function ($query) use (&$capturedSql) {
|
||||
$capturedSql[] = $query->sql;
|
||||
});
|
||||
|
||||
// DDL(如 CREATE TABLE)在 MySQL 中会隐式提交事务,因此不包裹外层事务执行
|
||||
foreach ($newMigrations as $migration) {
|
||||
try {
|
||||
$migrationClass = require $migration[0];
|
||||
$migrationClass->up($schema);
|
||||
$output->writeln("<info>⬆️ Migrated: {$migration[1]}</info>");
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln("<error>❌ Error migrating {$migration[1]}: {$e->getMessage()}</error>");
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// 记录日志(在事务外,确保成功后才记录)
|
||||
$logLines = array_map(
|
||||
fn($item) => "{$batchNum}," . $item[1],
|
||||
$newMigrations
|
||||
);
|
||||
file_put_contents($logFile, PHP_EOL . implode(PHP_EOL, $logLines), FILE_APPEND | LOCK_EX);
|
||||
|
||||
if ($runSeed) {
|
||||
$this->runSeeders($output);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出迁移 SQL 到文件
|
||||
*/
|
||||
protected function exportMigrationsToSql(OutputInterface $output, array $newMigrations, $sqlOption): void
|
||||
{
|
||||
$connection = Db::connection($this->connection);
|
||||
$prefix = config('database.connections.mysql.prefix', 'ma_');
|
||||
|
||||
// 如果需要导出数据且不是干运行模式
|
||||
if ($this->exportWithData && !$this->dryRun) {
|
||||
// 先运行 seeders(插入初始数据)
|
||||
$output->writeln("<comment>🔄 Running seeders before SQL export...</comment>");
|
||||
$this->runSeeders($output);
|
||||
$output->writeln("<comment>✅ Seeders completed, now generating SQL...</comment>");
|
||||
}
|
||||
|
||||
// 生成 SQL 文件
|
||||
$sqlContent = "-- Migration SQL Export\n";
|
||||
$sqlContent .= "-- Generated at: " . date('Y-m-d H:i:s') . "\n";
|
||||
$sqlContent .= "-- Connection: {$this->connection}\n";
|
||||
$sqlContent .= "-- Include Data: " . ($this->exportWithData ? 'YES' : 'NO') . "\n\n";
|
||||
$sqlContent .= "SET FOREIGN_KEY_CHECKS = 0;\n\n";
|
||||
|
||||
// 从迁移文件解析并生成 SQL(通过分析迁移文件内容)
|
||||
$sqlStatements = $this->parseMigrationsToSql($connection, $newMigrations, $prefix, $this->exportWithData);
|
||||
$sqlContent .= $sqlStatements;
|
||||
|
||||
$sqlContent .= "SET FOREIGN_KEY_CHECKS = 1;\n";
|
||||
|
||||
// 保存文件
|
||||
$filename = $sqlOption === true
|
||||
? 'migration_' . date('YmdHis') . '.sql'
|
||||
: $sqlOption;
|
||||
$filepath = base_path($filename);
|
||||
|
||||
file_put_contents($filepath, $sqlContent);
|
||||
|
||||
$output->writeln("<info>📄 SQL exported to: {$filepath}</info>");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析迁移文件生成 SQL
|
||||
*/
|
||||
protected function parseMigrationsToSql($connection, array $migrations, string $prefix, bool $exportData = true): string
|
||||
{
|
||||
$sqlContent = '';
|
||||
|
||||
foreach ($migrations as $migration) {
|
||||
$migrationName = $migration[1];
|
||||
$tableName = $this->extractTableNameFromMigration($migrationName);
|
||||
$fullTableName = $prefix . $tableName;
|
||||
|
||||
$sqlContent .= "-- Migration: {$migrationName}\n";
|
||||
$sqlContent .= "-- Table: {$fullTableName}\n\n";
|
||||
|
||||
// 尝试从数据库获取表结构
|
||||
$createSql = $this->generateCreateTableSql($connection, $fullTableName);
|
||||
|
||||
if ($createSql) {
|
||||
// 表已存在,使用现有结构
|
||||
$sqlContent .= "DROP TABLE IF EXISTS `{$fullTableName}`;\n";
|
||||
$sqlContent .= $createSql . ";\n\n";
|
||||
|
||||
// 导出表中的数据(如果启用)
|
||||
if ($exportData) {
|
||||
$insertSql = $this->generateInsertDataSql($connection, $fullTableName);
|
||||
if ($insertSql) {
|
||||
$sqlContent .= "-- Data from {$fullTableName}\n";
|
||||
$sqlContent .= $insertSql . "\n\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 表不存在,生成基本的 CREATE TABLE 语句
|
||||
$sqlContent .= "-- Table {$fullTableName} does not exist, please run migration first\n";
|
||||
$sqlContent .= "-- Or manually create the table based on: {$migrationName}\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $sqlContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 INSERT 数据 SQL
|
||||
*/
|
||||
protected function generateInsertDataSql($connection, string $tableName): ?string
|
||||
{
|
||||
try {
|
||||
$rows = $connection->select("SELECT * FROM `{$tableName}`");
|
||||
if (empty($rows)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$insertStatements = [];
|
||||
foreach ($rows as $row) {
|
||||
$row = (array)$row;
|
||||
$columns = array_keys($row);
|
||||
$values = array_values($row);
|
||||
|
||||
// 处理值
|
||||
$escapedValues = array_map(function ($value) use ($connection) {
|
||||
if ($value === null) {
|
||||
return 'NULL';
|
||||
}
|
||||
return "'" . $connection->getPdo()->quote($value) . "'";
|
||||
}, $values);
|
||||
|
||||
$insertStatements[] = sprintf(
|
||||
"INSERT INTO `{$tableName}` (`%s`) VALUES (%s);",
|
||||
implode('`, `', $columns),
|
||||
implode(', ', $escapedValues)
|
||||
);
|
||||
}
|
||||
|
||||
return implode("\n", $insertStatements);
|
||||
} catch (\Throwable) {
|
||||
// 如果查询失败(如视图),返回 null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从迁移文件名提取表名
|
||||
*/
|
||||
protected function extractTableNameFromMigration(string $migrationName): string
|
||||
{
|
||||
// 例如: 2025_10_10_162850_create_cache -> cache
|
||||
// 例如: 2025_10_10_162851_create_sys_admin -> sys_admin
|
||||
if (preg_match('/create_(\w+)$/', $migrationName, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
// 对于修改表的迁移,尝试提取表名
|
||||
if (preg_match('/_(add|alter|drop|optimize)_(.+)$/', $migrationName, $matches)) {
|
||||
return $matches[2];
|
||||
}
|
||||
return $migrationName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 CREATE TABLE SQL
|
||||
*/
|
||||
protected function generateCreateTableSql($connection, string $tableName): ?string
|
||||
{
|
||||
try {
|
||||
$result = $connection->select("SHOW CREATE TABLE `{$tableName}`");
|
||||
if (!empty($result)) {
|
||||
$row = (array)$result[0];
|
||||
return $row['Create Table'] ?? $row['Create View'] ?? null;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// 表不存在,忽略
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚
|
||||
*
|
||||
* @param \Symfony\Component\Console\Output\OutputInterface $output
|
||||
* @param int|null $batchNum
|
||||
* @param string|bool $sqlOption
|
||||
*/
|
||||
protected function runRollback(OutputInterface $output, ?int $batchNum = null, $sqlOption = false)
|
||||
{
|
||||
$logFile = $this->getMigrationLogFile();
|
||||
$rows = $this->fetchMigrationRows($logFile);
|
||||
|
||||
if (empty($rows)) {
|
||||
$output->writeln("<comment>ℹ️ No migrations to rollback</comment>");
|
||||
return;
|
||||
}
|
||||
|
||||
if ($batchNum !== null) {
|
||||
// 回滚到指定批次
|
||||
$rollbackBatch = array_filter($rows, fn($row) => (int)$row[0] > $batchNum);
|
||||
if (empty($rollbackBatch)) {
|
||||
$output->writeln("<comment>ℹ️ No migrations to rollback for batch {$batchNum}</comment>");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 回滚最新批次(默认行为)
|
||||
$latestBatch = (int)end($rows)[0];
|
||||
$rollbackBatch = array_filter($rows, fn($row) => (int)$row[0] === $latestBatch);
|
||||
}
|
||||
|
||||
$rollbackBatch = array_reverse($rollbackBatch);
|
||||
|
||||
if ($this->dryRun) {
|
||||
$output->writeln("<comment>🔍 Migrations to rollback:</comment>");
|
||||
foreach ($rollbackBatch as $item) {
|
||||
$output->writeln(" - {$item[1]}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是导出 SQL 模式
|
||||
if ($this->exportSql) {
|
||||
$this->exportRollbackToSql($output, $rollbackBatch, $sqlOption);
|
||||
return;
|
||||
}
|
||||
|
||||
$schema = $this->getSchemaBuilderWithInnoDB();
|
||||
|
||||
// 启用事务
|
||||
Db::connection($this->connection)->transaction(function () use ($output, $rollbackBatch, $schema) {
|
||||
foreach ($rollbackBatch as $item) {
|
||||
try {
|
||||
$migrationClass = require base_path("resource/database/migrations/{$item[1]}.php");
|
||||
$migrationClass->down($schema);
|
||||
$output->writeln("<info>⬇️ Rolled back: {$item[1]}</info>");
|
||||
} catch (\Throwable $e) {
|
||||
$output->writeln("<error>❌ Error rolling back {$item[1]}: {$e->getMessage()}</error>");
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 清理日志
|
||||
$latestBatch = (int)end($rows)[0];
|
||||
$remainingLogs = array_filter($rows, fn($row) => $batchNum !== null
|
||||
? (int)$row[0] <= $batchNum
|
||||
: (int)$row[0] < $latestBatch);
|
||||
file_put_contents($logFile, implode(PHP_EOL, array_map(fn($row) => implode(',', $row), $remainingLogs)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出回滚 SQL 到文件
|
||||
*/
|
||||
protected function exportRollbackToSql(OutputInterface $output, array $rollbackBatch, $sqlOption): void
|
||||
{
|
||||
$prefix = config('database.connections.mysql.prefix', 'ma_');
|
||||
|
||||
// 生成 SQL 文件
|
||||
$sqlContent = "-- Rollback SQL Export\n";
|
||||
$sqlContent .= "-- Generated at: " . date('Y-m-d H:i:s') . "\n";
|
||||
$sqlContent .= "-- Connection: {$this->connection}\n\n";
|
||||
$sqlContent .= "SET FOREIGN_KEY_CHECKS = 0;\n\n";
|
||||
|
||||
// 为每个要回滚的迁移生成 DROP TABLE 语句
|
||||
foreach ($rollbackBatch as $item) {
|
||||
$tableName = $this->extractTableNameFromMigration($item[1]);
|
||||
$fullTableName = $prefix . $tableName;
|
||||
|
||||
// 尝试获取表结构用于生成 DROP 语句
|
||||
$sqlContent .= "-- Rollback: {$item[1]}\n";
|
||||
$sqlContent .= "-- Table: {$fullTableName}\n";
|
||||
|
||||
// 添加 DROP TABLE 语句
|
||||
$sqlContent .= "DROP TABLE IF EXISTS `{$fullTableName}`;\n\n";
|
||||
}
|
||||
|
||||
$sqlContent .= "SET FOREIGN_KEY_CHECKS = 1;\n";
|
||||
|
||||
// 保存文件
|
||||
$filename = $sqlOption === true
|
||||
? 'rollback_' . date('YmdHis') . '.sql'
|
||||
: $sqlOption;
|
||||
$filepath = base_path($filename);
|
||||
|
||||
file_put_contents($filepath, $sqlContent);
|
||||
|
||||
$output->writeln("<info>📄 Rollback SQL exported to: {$filepath}</info>");
|
||||
}
|
||||
|
||||
protected function getMigrationLogFile(): string
|
||||
{
|
||||
return runtime_path('migrations/' . ($this->connection ?? 'default') . '-migrations.log');
|
||||
}
|
||||
|
||||
protected function fetchMigrationRows(string $logFile): array
|
||||
{
|
||||
if (!file_exists($logFile)) {
|
||||
touch($logFile);
|
||||
}
|
||||
$content = trim(file_get_contents($logFile));
|
||||
return empty($content) ? [] : array_map(fn($row) => explode(',', $row), explode(PHP_EOL, $content));
|
||||
}
|
||||
|
||||
protected function getSchemaBuilder(): \Illuminate\Database\Schema\Builder
|
||||
{
|
||||
$schema = Db::connection($this->connection)->getSchemaBuilder();
|
||||
// 设置默认使用 InnoDB 引擎
|
||||
$schema->blueprintResolver(function ($table, $callback) {
|
||||
return new \Illuminate\Database\Schema\Blueprint($table, $callback);
|
||||
});
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取带引擎配置的表创建回调
|
||||
*/
|
||||
protected function getSchemaBuilderWithInnoDB(): \Illuminate\Database\Schema\Builder
|
||||
{
|
||||
$schema = $this->getSchemaBuilder();
|
||||
// 确保使用 InnoDB 引擎
|
||||
Db::connection($this->connection)->statement("SET default_storage_engine=INNODB");
|
||||
return $schema;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\make;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 生成控制器命令
|
||||
* 使用方法:
|
||||
* - 生成控制器:php webman make:controller TestController
|
||||
* - 生成带完整路径的控制器:php webman make:controller adminapi/controller/system/TestController
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-make:controller',
|
||||
description: 'Generate a new controller class',
|
||||
aliases: ['madong-make:controller'],
|
||||
hidden: false
|
||||
)]
|
||||
class ControllerCommand extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('name', InputArgument::REQUIRED, 'Controller name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Controller Generator');
|
||||
|
||||
$name = $input->getArgument('name');
|
||||
$io->info("Generating controller: {$name}");
|
||||
|
||||
// 处理路径和命名空间
|
||||
$name = str_replace('\\', '/', $name);
|
||||
$parts = explode('/', $name);
|
||||
$className = ucfirst(array_pop($parts));
|
||||
|
||||
// 构建文件路径和命名空间
|
||||
if (empty($parts)) {
|
||||
// 如果没有提供路径,默认创建到app/controller目录
|
||||
$file = app_path() . DIRECTORY_SEPARATOR . 'controller' . DIRECTORY_SEPARATOR . "{$className}.php";
|
||||
$namespace = 'app\controller';
|
||||
} else {
|
||||
// 使用用户提供的完整路径
|
||||
$relativePath = implode(DIRECTORY_SEPARATOR, $parts);
|
||||
$file = app_path() . DIRECTORY_SEPARATOR . $relativePath . DIRECTORY_SEPARATOR . "{$className}.php";
|
||||
$namespace = 'app\\' . str_replace('/', '\\', $relativePath);
|
||||
}
|
||||
|
||||
if (is_file($file)) {
|
||||
if (!$this->confirm($io, "$file already exists. Do you want to override it?")) {
|
||||
$io->warning('Operation cancelled.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
$this->createController($className, $namespace, $file);
|
||||
$io->success("Controller {$className} generated successfully!");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建控制器文件
|
||||
*/
|
||||
protected function createController(string $className, string $namespace, string $file): void
|
||||
{
|
||||
$path = pathinfo($file, PATHINFO_DIRNAME);
|
||||
if (!is_dir($path)) {
|
||||
mkdir($path, 0777, true);
|
||||
}
|
||||
|
||||
// 读取模板文件
|
||||
$template = file_get_contents(__DIR__ . '/stubs/controller.stub');
|
||||
|
||||
// 替换占位符
|
||||
$content = str_replace(
|
||||
['DummyNamespace', 'DummyClass'],
|
||||
[$namespace, $className],
|
||||
$template
|
||||
);
|
||||
|
||||
file_put_contents($file, $content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\make;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 生成Dao命令
|
||||
* 使用方法:
|
||||
* - 生成Dao:php webman make:dao TestDao
|
||||
* - 生成带完整路径的Dao:php webman make:dao system/TestDao
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-make:dao',
|
||||
description: 'Generate a new DAO class',
|
||||
aliases: ['madong-make:dao'],
|
||||
hidden: false
|
||||
)]
|
||||
class DaoCommand extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('name', InputArgument::REQUIRED, 'DAO name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('DAO Generator');
|
||||
|
||||
$name = $input->getArgument('name');
|
||||
$io->info("Generating DAO: {$name}");
|
||||
|
||||
// 处理路径和命名空间
|
||||
$name = str_replace('\\', '/', $name);
|
||||
$parts = explode('/', $name);
|
||||
$className = ucfirst(array_pop($parts));
|
||||
|
||||
// 构建文件路径和命名空间
|
||||
if (empty($parts)) {
|
||||
// 如果没有提供路径,默认创建到app/dao目录
|
||||
$file = app_path() . DIRECTORY_SEPARATOR . 'dao' . DIRECTORY_SEPARATOR . "{$className}.php";
|
||||
$namespace = 'app\dao';
|
||||
} else {
|
||||
// 使用用户提供的完整路径
|
||||
$relativePath = implode(DIRECTORY_SEPARATOR, $parts);
|
||||
$file = app_path() . DIRECTORY_SEPARATOR . 'dao' . DIRECTORY_SEPARATOR . $relativePath . DIRECTORY_SEPARATOR . "{$className}.php";
|
||||
$namespace = 'app\dao\\' . str_replace('/', '\\', $relativePath);
|
||||
}
|
||||
|
||||
if (is_file($file)) {
|
||||
if (!$this->confirm($io, "$file already exists. Do you want to override it?")) {
|
||||
$io->warning('Operation cancelled.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
// 提取模型名称
|
||||
$modelName = str_replace('Dao', '', $className);
|
||||
if ($modelName === $className) {
|
||||
$modelName .= 'Model';
|
||||
}
|
||||
|
||||
$this->createDao($className, $namespace, $modelName, $file);
|
||||
$io->success("DAO {$className} generated successfully!");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Dao文件
|
||||
*/
|
||||
protected function createDao(string $className, string $namespace, string $modelName, string $file): void
|
||||
{
|
||||
$path = pathinfo($file, PATHINFO_DIRNAME);
|
||||
if (!is_dir($path)) {
|
||||
mkdir($path, 0777, true);
|
||||
}
|
||||
|
||||
// 读取模板文件
|
||||
$template = file_get_contents(__DIR__ . '/stubs/dao.stub');
|
||||
|
||||
// 替换占位符
|
||||
$content = str_replace(
|
||||
['DummyNamespace', 'DummyClass', 'DummyModel'],
|
||||
[$namespace, $className, $modelName],
|
||||
$template
|
||||
);
|
||||
|
||||
file_put_contents($file, $content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\make;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 生成中间件命令
|
||||
* 使用方法:
|
||||
* - 生成中间件:php webman make:middleware TestMiddleware
|
||||
* - 生成带完整路径的中间件:php webman make:middleware admin/TestMiddleware
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-make:middleware',
|
||||
description: 'Generate a new middleware class',
|
||||
aliases: ['madong-make:middleware'],
|
||||
hidden: false
|
||||
)]
|
||||
class MiddlewareCommand extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('name', InputArgument::REQUIRED, 'Middleware name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Middleware Generator');
|
||||
|
||||
$name = $input->getArgument('name');
|
||||
$io->info("Generating middleware: {$name}");
|
||||
|
||||
// 处理路径和命名空间
|
||||
$name = str_replace('\\', '/', $name);
|
||||
$parts = explode('/', $name);
|
||||
$className = ucfirst(array_pop($parts));
|
||||
|
||||
// 构建文件路径和命名空间
|
||||
if (empty($parts)) {
|
||||
// 如果没有提供路径,默认创建到app/middleware目录
|
||||
$file = app_path() . DIRECTORY_SEPARATOR . 'middleware' . DIRECTORY_SEPARATOR . "{$className}.php";
|
||||
$namespace = 'app\middleware';
|
||||
} else {
|
||||
// 使用用户提供的完整路径
|
||||
$relativePath = implode(DIRECTORY_SEPARATOR, $parts);
|
||||
$file = app_path() . DIRECTORY_SEPARATOR . $relativePath . DIRECTORY_SEPARATOR . "{$className}.php";
|
||||
$namespace = 'app\\' . str_replace('/', '\\', $relativePath);
|
||||
}
|
||||
|
||||
if (is_file($file)) {
|
||||
if (!$this->confirm($io, "$file already exists. Do you want to override it?")) {
|
||||
$io->warning('Operation cancelled.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
$this->createMiddleware($className, $namespace, $file);
|
||||
$io->success("Middleware {$className} generated successfully!");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建中间件文件
|
||||
*/
|
||||
protected function createMiddleware(string $className, string $namespace, string $file): void
|
||||
{
|
||||
$path = pathinfo($file, PATHINFO_DIRNAME);
|
||||
if (!is_dir($path)) {
|
||||
mkdir($path, 0777, true);
|
||||
}
|
||||
|
||||
// 读取模板文件
|
||||
$template = file_get_contents(__DIR__ . '/stubs/middleware.stub');
|
||||
|
||||
// 替换占位符
|
||||
$content = str_replace(
|
||||
['DummyNamespace', 'DummyClass'],
|
||||
[$namespace, $className],
|
||||
$template
|
||||
);
|
||||
|
||||
file_put_contents($file, $content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\make;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 生成模型层命令
|
||||
* 使用方法:
|
||||
* - 生成模型:php webman make:model TestModel
|
||||
* - 生成带完整路径的模型:php webman make:model system/TestModel
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-make:model',
|
||||
description: 'Generate a new model class',
|
||||
aliases: ['madong-make:model'],
|
||||
hidden: false
|
||||
)]
|
||||
class ModelCommand extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('name', InputArgument::REQUIRED, 'Model name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Model Generator');
|
||||
|
||||
$name = $input->getArgument('name');
|
||||
$io->info("Generating model: {$name}");
|
||||
|
||||
// 处理路径和命名空间
|
||||
$name = str_replace('\\', '/', $name);
|
||||
$parts = explode('/', $name);
|
||||
$className = ucfirst(array_pop($parts));
|
||||
|
||||
// 构建文件路径和命名空间
|
||||
if (empty($parts)) {
|
||||
// 如果没有提供路径,默认创建到app/model目录
|
||||
$file = app_path() . DIRECTORY_SEPARATOR . 'model' . DIRECTORY_SEPARATOR . "{$className}.php";
|
||||
$namespace = 'app\model';
|
||||
} else {
|
||||
// 使用用户提供的完整路径
|
||||
$relativePath = implode(DIRECTORY_SEPARATOR, $parts);
|
||||
$file = app_path() . DIRECTORY_SEPARATOR . 'model' . DIRECTORY_SEPARATOR . $relativePath . DIRECTORY_SEPARATOR . "{$className}.php";
|
||||
$namespace = 'app\model\\' . str_replace('/', '\\', $relativePath);
|
||||
}
|
||||
|
||||
if (is_file($file)) {
|
||||
if (!$this->confirm($io, "$file already exists. Do you want to override it?")) {
|
||||
$io->warning('Operation cancelled.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成表名
|
||||
$tableName = 'sys_' . strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $className));
|
||||
|
||||
$this->createModel($className, $namespace, $tableName, $file);
|
||||
$io->success("Model {$className} generated successfully!");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模型文件
|
||||
*/
|
||||
protected function createModel(string $className, string $namespace, string $tableName, string $file): void
|
||||
{
|
||||
$path = pathinfo($file, PATHINFO_DIRNAME);
|
||||
if (!is_dir($path)) {
|
||||
mkdir($path, 0777, true);
|
||||
}
|
||||
|
||||
// 读取模板文件
|
||||
$template = file_get_contents(__DIR__ . '/stubs/model.stub');
|
||||
|
||||
// 替换占位符
|
||||
$content = str_replace(
|
||||
['DummyNamespace', 'DummyClass', 'DummyTable'],
|
||||
[$namespace, $className, $tableName],
|
||||
$template
|
||||
);
|
||||
|
||||
file_put_contents($file, $content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\make;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 生成服务层命令
|
||||
* 使用方法:
|
||||
* - 生成服务:php webman make:service TestService
|
||||
* - 生成带完整路径的服务:php webman make:service admin/system/TestService
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-make:service',
|
||||
description: 'Generate a new service class',
|
||||
aliases: ['madong-make:service'],
|
||||
hidden: false
|
||||
)]
|
||||
class ServiceCommand extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('name', InputArgument::REQUIRED, 'Service name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Service Generator');
|
||||
|
||||
$name = $input->getArgument('name');
|
||||
$io->info("Generating service: {$name}");
|
||||
|
||||
// 处理路径和命名空间
|
||||
$name = str_replace('\\', '/', $name);
|
||||
$parts = explode('/', $name);
|
||||
$className = ucfirst(array_pop($parts));
|
||||
|
||||
// 构建文件路径和命名空间
|
||||
if (empty($parts)) {
|
||||
// 如果没有提供路径,默认创建到app/service目录
|
||||
$file = app_path() . DIRECTORY_SEPARATOR . 'service' . DIRECTORY_SEPARATOR . "{$className}.php";
|
||||
$namespace = 'app\service';
|
||||
} else {
|
||||
// 使用用户提供的完整路径
|
||||
$relativePath = implode(DIRECTORY_SEPARATOR, $parts);
|
||||
$file = app_path() . DIRECTORY_SEPARATOR . 'service' . DIRECTORY_SEPARATOR . $relativePath . DIRECTORY_SEPARATOR . "{$className}.php";
|
||||
$namespace = 'app\service\\' . str_replace('/', '\\', $relativePath);
|
||||
}
|
||||
|
||||
if (is_file($file)) {
|
||||
if (!$this->confirm($io, "$file already exists. Do you want to override it?")) {
|
||||
$io->warning('Operation cancelled.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
// 提取Dao名称
|
||||
$daoName = str_replace('Service', 'Dao', $className);
|
||||
if ($daoName === $className) {
|
||||
$daoName .= 'Dao';
|
||||
}
|
||||
|
||||
$this->createService($className, $namespace, $daoName, $file);
|
||||
$io->success("Service {$className} generated successfully!");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建服务文件
|
||||
*/
|
||||
protected function createService(string $className, string $namespace, string $daoName, string $file): void
|
||||
{
|
||||
$path = pathinfo($file, PATHINFO_DIRNAME);
|
||||
if (!is_dir($path)) {
|
||||
mkdir($path, 0777, true);
|
||||
}
|
||||
|
||||
// 读取模板文件
|
||||
$template = file_get_contents(__DIR__ . '/stubs/service.stub');
|
||||
|
||||
// 替换占位符
|
||||
$content = str_replace(
|
||||
['DummyNamespace', 'DummyClass', 'DummyDao'],
|
||||
[$namespace, $className, $daoName],
|
||||
$template
|
||||
);
|
||||
|
||||
file_put_contents($file, $content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\make;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 生成验证层命令
|
||||
* 使用方法:
|
||||
* - 生成验证器:php webman make:validate TestValidate
|
||||
* - 生成带完整路径的验证器:php webman make:validate adminapi/validate/system/TestValidate
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-make:validate',
|
||||
description: 'Generate a new validate class',
|
||||
aliases: ['madong-make:validate'],
|
||||
hidden: false
|
||||
)]
|
||||
class ValidateCommand extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('name', InputArgument::REQUIRED, 'Validate name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Validate Generator');
|
||||
|
||||
$name = $input->getArgument('name');
|
||||
$io->info("Generating validate: {$name}");
|
||||
|
||||
// 处理路径和命名空间
|
||||
$name = str_replace('\\', '/', $name);
|
||||
$parts = explode('/', $name);
|
||||
$className = ucfirst(array_pop($parts));
|
||||
|
||||
// 构建文件路径和命名空间
|
||||
if (empty($parts)) {
|
||||
// 如果没有提供路径,默认创建到app/validate目录
|
||||
$file = app_path() . DIRECTORY_SEPARATOR . 'validate' . DIRECTORY_SEPARATOR . "{$className}.php";
|
||||
$namespace = 'app\validate';
|
||||
} else {
|
||||
// 使用用户提供的完整路径
|
||||
$relativePath = implode(DIRECTORY_SEPARATOR, $parts);
|
||||
$file = app_path() . DIRECTORY_SEPARATOR . $relativePath . DIRECTORY_SEPARATOR . "{$className}.php";
|
||||
$namespace = 'app\\' . str_replace('/', '\\', $relativePath);
|
||||
}
|
||||
|
||||
if (is_file($file)) {
|
||||
if (!$this->confirm($io, "$file already exists. Do you want to override it?")) {
|
||||
$io->warning('Operation cancelled.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
$this->createValidate($className, $namespace, $file);
|
||||
$io->success("Validate {$className} generated successfully!");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建验证器文件
|
||||
*/
|
||||
protected function createValidate(string $className, string $namespace, string $file): void
|
||||
{
|
||||
$path = pathinfo($file, PATHINFO_DIRNAME);
|
||||
if (!is_dir($path)) {
|
||||
mkdir($path, 0777, true);
|
||||
}
|
||||
|
||||
// 读取模板文件
|
||||
$template = file_get_contents(__DIR__ . '/stubs/validate.stub');
|
||||
|
||||
// 替换占位符
|
||||
$content = str_replace(
|
||||
['DummyNamespace', 'DummyClass'],
|
||||
[$namespace, $className],
|
||||
$template
|
||||
);
|
||||
|
||||
file_put_contents($file, $content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: https://madong.tech
|
||||
*/
|
||||
|
||||
namespace DummyNamespace;
|
||||
|
||||
use core\base\BaseController;
|
||||
use support\Request;
|
||||
use app\adminapi\controller\Crud;
|
||||
|
||||
/**
|
||||
* DummyClass控制器
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0
|
||||
*/
|
||||
final class DummyClass extends Crud
|
||||
{
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: https://madong.tech
|
||||
*/
|
||||
|
||||
namespace DummyNamespace;
|
||||
|
||||
use app\model\DummyModel;
|
||||
use core\base\BaseDao;
|
||||
|
||||
/**
|
||||
* DummyClass数据访问对象
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0
|
||||
*/
|
||||
class DummyClass extends BaseDao
|
||||
{
|
||||
/**
|
||||
* 设置模型类
|
||||
*/
|
||||
protected function setModel(): string
|
||||
{
|
||||
return DummyModel::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: https://madong.tech
|
||||
*/
|
||||
|
||||
namespace DummyNamespace;
|
||||
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use Webman\MiddlewareInterface;
|
||||
|
||||
/**
|
||||
* DummyClass中间件
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0
|
||||
*/
|
||||
class DummyClass implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* 处理请求
|
||||
*/
|
||||
public function process(Request \$request, callable \$handler): Response
|
||||
{
|
||||
// 前置中间件逻辑
|
||||
\$response = \$handler(\$request);
|
||||
// 后置中间件逻辑
|
||||
return \$response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: https://madong.tech
|
||||
*/
|
||||
|
||||
namespace DummyNamespace;
|
||||
|
||||
use core\base\BaseModel;
|
||||
|
||||
/**
|
||||
* DummyClass模型
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0
|
||||
*/
|
||||
class DummyClass extends BaseModel
|
||||
{
|
||||
/**
|
||||
* 数据表名称
|
||||
*/
|
||||
protected \$table = 'DummyTable';
|
||||
|
||||
/**
|
||||
* 数据表主键
|
||||
*/
|
||||
protected \$primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* 可批量赋值的字段
|
||||
*/
|
||||
protected \$fillable = [
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* 追加字段
|
||||
*/
|
||||
protected \$appends = [
|
||||
'created_date',
|
||||
'updated_date',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: https://madong.tech
|
||||
*/
|
||||
|
||||
namespace DummyNamespace;
|
||||
|
||||
use app\dao\DummyDao;
|
||||
use core\base\BaseService;
|
||||
|
||||
/**
|
||||
* DummyClass服务类
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0
|
||||
*/
|
||||
class DummyClass extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造方法
|
||||
*/
|
||||
public function __construct(DummyDao \$dao)
|
||||
{
|
||||
\$this->dao = \$dao;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: https://madong.tech
|
||||
*/
|
||||
|
||||
namespace DummyNamespace;
|
||||
|
||||
use core\base\BaseValidate;
|
||||
|
||||
/**
|
||||
* DummyClass验证器
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0
|
||||
*/
|
||||
class DummyClass extends BaseValidate
|
||||
{
|
||||
/**
|
||||
* 定义验证规则
|
||||
*/
|
||||
protected array \$rule = [
|
||||
// 示例规则
|
||||
// 'name' => 'require|max:50',
|
||||
// 'code' => 'require|alphaDash',
|
||||
// 'enabled' => 'require',
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义错误信息
|
||||
*/
|
||||
protected array \$message = [
|
||||
// 示例错误信息
|
||||
// 'name.require' => '名称必须填写',
|
||||
// 'name.max' => '名称最多50个字符',
|
||||
// 'code.require' => '标识必须填写',
|
||||
// 'code.alphaDash' => '标识只能由英文字母组成',
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义场景
|
||||
*/
|
||||
protected array \$scene = [
|
||||
'store' => [
|
||||
// 创建时验证的字段
|
||||
],
|
||||
'update' => [
|
||||
// 更新时验证的字段
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\metadata;
|
||||
|
||||
use app\service\core\metadata\MetadataCollectorService;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use support\Container;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'madong:permission:collect',
|
||||
description: 'Collect permissions from controller annotations',
|
||||
aliases: ['madong:permission:collect'],
|
||||
hidden: false
|
||||
)]
|
||||
class CollectCommand extends Command
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this->addOption('output', 'o', InputOption::VALUE_OPTIONAL, 'Output file path', base_path('permissions.json'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @return int
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$outputPath = $input->getOption('output');
|
||||
|
||||
$output->writeln('<info>开始收集权限...</info>');
|
||||
|
||||
try {
|
||||
/** @var MetadataCollectorService $collector */
|
||||
$collector = Container::make(MetadataCollectorService::class);
|
||||
$permissions = $collector->collect();
|
||||
|
||||
// 导出为 JSON 文件
|
||||
$success = $collector->exportToJson($outputPath);
|
||||
|
||||
if ($success) {
|
||||
$output->writeln(sprintf('<info>权限收集完成,共收集到 %d 个权限</info>', count($permissions)));
|
||||
$output->writeln(sprintf('<info>权限列表已导出到:%s</info>', $outputPath));
|
||||
return Command::SUCCESS;
|
||||
} else {
|
||||
$output->writeln('<error>权限收集失败:无法写入文件</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln('<error>权限收集失败:' . $e->getMessage() . '</error>');
|
||||
$output->writeln('<error>错误堆栈:' . $e->getTraceAsString() . '</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
|
||||
namespace app\command\plugin;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use app\service\core\plugin\PluginUninstallService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 插件删除命令
|
||||
* 使用方法:
|
||||
* - 删除插件:php webman madong-plugin:delete plugin-name
|
||||
* - 移除插件(别名):php webman madong-plugin:remove plugin-name
|
||||
*
|
||||
* 注意:删除前必须先卸载插件
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-plugin:delete',
|
||||
description: 'Delete plugin package from the system (requires plugin to be uninstalled first)',
|
||||
aliases: ['madong-plugin:delete', 'madong-plugin:remove'],
|
||||
hidden: false
|
||||
)]
|
||||
class DeleteCommand extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('name', InputArgument::REQUIRED, 'Plugin name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Plugin Deleter');
|
||||
|
||||
// 获取参数
|
||||
$name = $input->getArgument('name');
|
||||
|
||||
$io->info("Deleting plugin: {$name}");
|
||||
$io->note("Note: Plugin must be uninstalled before deletion");
|
||||
|
||||
// 确认删除
|
||||
if (!$this->confirm($io, "Are you sure you want to delete plugin {$name}? This action cannot be undone.")) {
|
||||
$io->note("Deletion cancelled");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建删除服务实例(统一入口)
|
||||
$deleteService = new PluginUninstallService();
|
||||
|
||||
// 执行删除流程
|
||||
$result = $deleteService->delete($name);
|
||||
|
||||
if ($result) {
|
||||
return $this->outputSuccess($io, "Plugin '{$name}' deleted successfully!");
|
||||
} else {
|
||||
return $this->outputError($io, "Plugin '{$name}' deletion failed!");
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->outputError($io, sprintf("Deletion failed: %s", $e->getMessage()), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\plugin;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use app\service\core\plugin\PluginDevelopService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 打包插件命令
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-plugin:develop:build',
|
||||
description: 'Build plugin',
|
||||
aliases: ['madong-plugin:dev:build'],
|
||||
hidden: false
|
||||
)]
|
||||
class DevelopBuildCommand extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('key', InputArgument::REQUIRED, 'Plugin key (kebab-case format, e.g. test-demo)')
|
||||
->addOption('target', 't', InputOption::VALUE_OPTIONAL, 'Target directory path', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Build Plugin');
|
||||
|
||||
// 获取参数
|
||||
$pluginKey = $input->getArgument('key');
|
||||
$targetDirectory = $input->getOption('target');
|
||||
|
||||
$io->info(sprintf("Building plugin: %s", $pluginKey));
|
||||
if ($targetDirectory) {
|
||||
$io->info(sprintf("Target directory: %s", $targetDirectory));
|
||||
} else {
|
||||
$io->info("Target directory: Default (runtime/plugins)");
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建插件开发服务实例
|
||||
$pluginDevelopService = new PluginDevelopService();
|
||||
|
||||
// 构建插件
|
||||
$result = $pluginDevelopService->build($pluginKey, $targetDirectory);
|
||||
|
||||
if ($result['code'] === 200) {
|
||||
$io->success("Plugin built successfully!");
|
||||
$io->text(sprintf("ZIP file path: %s", $result['data']['zip_file_path']));
|
||||
return parent::SUCCESS;
|
||||
} else {
|
||||
return $this->outputError($io, sprintf("Build failed: %s", $result['message']));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->outputError($io, sprintf("Build failed: %s", $e->getMessage()), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\plugin;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use app\service\core\plugin\PluginDevelopService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 创建插件模板命令
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-plugin:develop:create',
|
||||
description: 'Create plugin template',
|
||||
aliases: ['madong-plugin:dev:create'],
|
||||
hidden: false
|
||||
)]
|
||||
class DevelopCreateCommand extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('name', InputArgument::REQUIRED, 'Plugin name (snake_case format, e.g. test_demo)')
|
||||
->addArgument('title', InputArgument::REQUIRED, 'Plugin title')
|
||||
->addArgument('description', InputArgument::OPTIONAL, 'Plugin description', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Create Plugin Template');
|
||||
|
||||
// 获取参数
|
||||
$pluginName = $input->getArgument('name');
|
||||
$pluginTitle = $input->getArgument('title');
|
||||
$pluginDescription = $input->getArgument('description');
|
||||
|
||||
$io->info(sprintf("Creating plugin template: %s", $pluginName));
|
||||
$io->info(sprintf("Plugin title: %s", $pluginTitle));
|
||||
$io->info(sprintf("Plugin description: %s", $pluginDescription));
|
||||
|
||||
try {
|
||||
// 创建插件开发服务实例
|
||||
$pluginDevelopService = new PluginDevelopService();
|
||||
|
||||
// 生成插件模板
|
||||
$result = $pluginDevelopService->generatePluginTemplate($pluginName, $pluginTitle, $pluginDescription);
|
||||
|
||||
if ($result['code'] === 200) {
|
||||
$io->success("Plugin template created successfully!");
|
||||
$io->text(sprintf("Frontend path: %s", $result['data']['frontend_path']));
|
||||
$io->text(sprintf("Backend path: %s", $result['data']['backend_path']));
|
||||
return parent::SUCCESS;
|
||||
} else {
|
||||
return $this->outputError($io, sprintf("Creation failed: %s", $result['message']));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->outputError($io, sprintf("Creation failed: %s", $e->getMessage()), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\plugin;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 删除插件命令
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-plugin:develop:delete',
|
||||
description: 'Delete plugin',
|
||||
aliases: ['madong-plugin:dev:delete'],
|
||||
hidden: false
|
||||
)]
|
||||
class DevelopDeleteCommand extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('key', InputArgument::REQUIRED, 'Plugin key (kebab-case format, e.g. test-demo)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Delete Plugin');
|
||||
|
||||
// 获取参数
|
||||
$pluginKey = $input->getArgument('key');
|
||||
$pluginName = str_replace('-', '_', $pluginKey);
|
||||
|
||||
$io->info(sprintf("Deleting plugin: %s", $pluginKey));
|
||||
|
||||
try {
|
||||
// 使用 base_path() 计算路径
|
||||
$basePath = base_path();
|
||||
$projectPath = dirname($basePath);
|
||||
|
||||
// 定义路径
|
||||
$frontendPath = $projectPath . '/admin/src/apps/' . $pluginName;
|
||||
$backendPath = $basePath . '/plugin/' . $pluginName;
|
||||
$runtimePath = $basePath . '/runtime/plugins/' . $pluginKey;
|
||||
$runtimeZipPath = $basePath . '/runtime/plugins/' . $pluginKey . '.zip';
|
||||
$buildPath = $basePath . '/runtime/build/' . $pluginKey;
|
||||
|
||||
// 删除前端目录
|
||||
if (is_dir($frontendPath)) {
|
||||
$this->deleteDirectory($frontendPath);
|
||||
$io->text(sprintf("Deleted frontend directory: %s", $frontendPath));
|
||||
}
|
||||
|
||||
// 删除后端目录
|
||||
if (is_dir($backendPath)) {
|
||||
$this->deleteDirectory($backendPath);
|
||||
$io->text(sprintf("Deleted backend directory: %s", $backendPath));
|
||||
}
|
||||
|
||||
// 删除构建目录
|
||||
if (is_dir($buildPath)) {
|
||||
$this->deleteDirectory($buildPath);
|
||||
$io->text(sprintf("Deleted build directory: %s", $buildPath));
|
||||
}
|
||||
|
||||
// 删除运行时目录
|
||||
if (is_dir($runtimePath)) {
|
||||
$this->deleteDirectory($runtimePath);
|
||||
$io->text(sprintf("Deleted runtime directory: %s", $runtimePath));
|
||||
}
|
||||
|
||||
// 删除运行时ZIP文件
|
||||
if (file_exists($runtimeZipPath)) {
|
||||
unlink($runtimeZipPath);
|
||||
$io->text(sprintf("Deleted runtime ZIP file: %s", $runtimeZipPath));
|
||||
}
|
||||
|
||||
return $this->outputSuccess($io, "Plugin deleted successfully!");
|
||||
} catch (\Exception $e) {
|
||||
return $this->outputError($io, sprintf("Deletion failed: %s", $e->getMessage()), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\plugin;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use app\service\core\plugin\PluginInstallService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 插件安装命令
|
||||
* 使用方法:
|
||||
* - 安装插件:php webman madong-plugin:install plugin-name
|
||||
* - 从市场安装:php webman madong-plugin:install plugin-name market
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-plugin:install',
|
||||
description: 'Install plugin to the system',
|
||||
aliases: ['madong-plugin:install'],
|
||||
hidden: false
|
||||
)]
|
||||
class InstallCommand extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('name', InputArgument::REQUIRED, 'Plugin name')
|
||||
->addArgument('mode', InputArgument::OPTIONAL, 'Install mode (local/market)', 'local');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Plugin Installer');
|
||||
|
||||
// 获取参数
|
||||
$name = $input->getArgument('name');
|
||||
$mode = $input->getArgument('mode');
|
||||
|
||||
$io->info("Installing plugin: {$name}");
|
||||
$io->info("Install mode: {$mode}");
|
||||
|
||||
try {
|
||||
// 创建安装服务实例
|
||||
$installService = new PluginInstallService();
|
||||
|
||||
// 执行安装流程(使用基类的流式处理)
|
||||
return $this->executeStream($installService->install($name, $mode), $io, $name);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->outputError($io, sprintf("Installation failed: %s", $e->getMessage()), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\plugin;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use app\service\core\plugin\PluginUninstallService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 插件运行器命令
|
||||
*
|
||||
* 使用方法:
|
||||
* php webman madong-plugin:run plugin-name install
|
||||
* php webman madong-plugin:run plugin-name uninstall # 使用统一的卸载服务
|
||||
* php webman madong-plugin:run plugin-name delete # 删除插件包
|
||||
* php webman madong-plugin:run plugin-name update
|
||||
* php webman madong-plugin:run plugin-name export
|
||||
*
|
||||
* 自动调用插件的 Install 类或对应的服务
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-plugin:run',
|
||||
description: 'Run plugin actions: install, uninstall, delete, update, export',
|
||||
aliases: ['madong-plugin:run'],
|
||||
hidden: false
|
||||
)]
|
||||
class RunCommand extends BaseCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('name', InputArgument::REQUIRED, 'Plugin name')
|
||||
->addArgument('action', InputArgument::REQUIRED, 'Action: install, uninstall, delete, update, export')
|
||||
->addArgument('version', InputArgument::OPTIONAL, 'Version (for update)')
|
||||
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force run migrations (ignore previous records)')
|
||||
->addOption('data', 'd', InputOption::VALUE_NONE, 'Export with data (for export action)')
|
||||
->addOption('output', 'o', InputOption::VALUE_OPTIONAL, 'Output file path (for export action)');
|
||||
}
|
||||
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$name = $input->getArgument('name');
|
||||
$action = $input->getArgument('action');
|
||||
$version = $input->getArgument('version') ?? '1.0.0';
|
||||
$force = $input->getOption('force');
|
||||
|
||||
$io->title("Plugin Runner: {$name}");
|
||||
$io->info("Action: {$action}" . ($force ? " (force)" : ""));
|
||||
|
||||
// 查找插件路径
|
||||
$pluginPath = base_path('plugin/' . $name);
|
||||
|
||||
if (!is_dir($pluginPath)) {
|
||||
$io->error("Plugin '{$name}' not found");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// 查找 Install 类(支持多个位置)
|
||||
$installFile = $pluginPath . '/Install.php';
|
||||
if (!file_exists($installFile)) {
|
||||
$installFile = $pluginPath . '/api/Install.php';
|
||||
}
|
||||
if (!file_exists($installFile)) {
|
||||
$installFile = $pluginPath . '/install/Install.php';
|
||||
}
|
||||
|
||||
if (!file_exists($installFile)) {
|
||||
$io->error("Install.php not found");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// 加载 Install 类
|
||||
include $installFile;
|
||||
|
||||
// 确定类名
|
||||
$className = "plugin\\{$name}\\Install";
|
||||
|
||||
if (!class_exists($className)) {
|
||||
$io->error("Install class not found: {$className}");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->info("Found: {$className}");
|
||||
|
||||
try {
|
||||
// 实例化并调用对应方法
|
||||
$instance = new $className();
|
||||
$io->info("Instance created: " . get_class($instance));
|
||||
|
||||
switch ($action) {
|
||||
case 'install':
|
||||
// Force migration: clear logs first
|
||||
if ($force && method_exists($instance, 'forceMigrate')) {
|
||||
$io->info('>>> Force migration mode - clearing logs...');
|
||||
$instance->forceMigrate();
|
||||
} elseif (!method_exists($instance, 'install')) {
|
||||
$io->warning('install() method not found');
|
||||
return Command::FAILURE;
|
||||
} else {
|
||||
$io->info('>>> Calling install() method...');
|
||||
$result = $instance->install($version);
|
||||
$io->info('<<< install() completed');
|
||||
}
|
||||
$io->success("Plugin '{$name}' installed successfully!");
|
||||
break;
|
||||
|
||||
case 'uninstall':
|
||||
// 使用统一的卸载服务(会检查 undeletable 配置)
|
||||
$io->info('>>> Calling uninstall via PluginUninstallService...');
|
||||
$uninstallService = new PluginUninstallService();
|
||||
return $this->executeStream($uninstallService->uninstall($name), $io, $name);
|
||||
|
||||
case 'delete':
|
||||
// 使用统一的删除服务(会检查卸载状态)
|
||||
$io->info('>>> Calling delete via PluginUninstallService...');
|
||||
$io->note("Plugin must be uninstalled before deletion");
|
||||
$deleteService = new PluginUninstallService();
|
||||
$result = $deleteService->delete($name);
|
||||
if ($result) {
|
||||
$io->success("Plugin '{$name}' deleted successfully!");
|
||||
return Command::SUCCESS;
|
||||
} else {
|
||||
$io->error("Plugin '{$name}' deletion failed!");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
case 'update':
|
||||
if (!method_exists($instance, 'update')) {
|
||||
$io->warning('update() method not found');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$oldVersion = $version;
|
||||
$io->info(">>> Calling update() from {$oldVersion}...");
|
||||
$result = $instance->update($oldVersion, $version);
|
||||
$io->info('<<< update() completed');
|
||||
$io->success("Plugin '{$name}' updated successfully!");
|
||||
break;
|
||||
|
||||
case 'export':
|
||||
if (!method_exists($instance, 'exportSql')) {
|
||||
$io->warning('exportSql() method not found');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$withData = $input->getOption('data');
|
||||
$outputFile = $input->getOption('output');
|
||||
$io->info('>>> Exporting SQL' . ($withData ? ' (with data)' : ' (structure only)') . '...');
|
||||
$result = $instance->exportSql($withData, $outputFile);
|
||||
if ($result) {
|
||||
$io->success("SQL exported successfully!");
|
||||
} else {
|
||||
$io->warning('Export failed or no tables found');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$io->error("Unknown action: {$action}");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return $this->outputError($io, "Error: " . $e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: http://www.madong.tech
|
||||
*/
|
||||
|
||||
namespace app\command\plugin;
|
||||
|
||||
use app\command\BaseCommand;
|
||||
use app\service\core\plugin\PluginUninstallService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* 插件卸载命令
|
||||
* 使用方法:
|
||||
* - 卸载插件:php webman madong-plugin:uninstall plugin-name
|
||||
*
|
||||
* @author Mr.April
|
||||
* @since 1.0.0
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'madong-plugin:uninstall',
|
||||
description: 'Uninstall plugin from the system',
|
||||
aliases: ['madong-plugin:uninstall'],
|
||||
hidden: false
|
||||
)]
|
||||
class UninstallCommand extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* 配置命令
|
||||
*/
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addArgument('name', InputArgument::REQUIRED, 'Plugin name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Plugin Uninstaller');
|
||||
|
||||
// 获取参数
|
||||
$name = $input->getArgument('name');
|
||||
|
||||
$io->info("Uninstalling plugin: {$name}");
|
||||
|
||||
// 确认卸载
|
||||
if (!$this->confirm($io, "Are you sure you want to uninstall plugin {$name}? This action cannot be undone.")) {
|
||||
$io->note("Uninstallation cancelled");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建卸载服务实例(统一入口)
|
||||
$uninstallService = new PluginUninstallService();
|
||||
|
||||
// 执行卸载流程(使用基类的流式处理)
|
||||
return $this->executeStream($uninstallService->uninstall($name), $io, $name);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->outputError($io, sprintf("Uninstallation failed: %s", $e->getMessage()), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user