Files
MrleeAdmin/backend/app/service/core/plugin/PluginUninstallService.php
T
2026-08-31 11:33:59 +08:00

239 lines
8.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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\service\core\plugin;
use core\tool\Sse;
use core\uuid\UUIDGenerator;
use support\Container;
/**
* 插件卸载服务(统一入口)
* CLI 和在线卸载都统一调用此服务
* CLI 模式通过迭代器获取 SSE 事件并解析
* 在线模式直接返回 Generator 给前端处理
*/
class PluginUninstallService extends PluginBaseService
{
/**
* 卸载插件(统一入口)
* 卸载流程统一使用插件自带的 Install.php
* PluginUninstallService 只负责流程控制,实际卸载逻辑完全由插件 Install.php 处理
*
* @param string $code 插件编码
*
* @return \Generator CLI 模式返回数组,在线模式返回 Generator
* @throws \Exception
*/
public function uninstall(string $code): \Generator
{
$sessionUuid = UUIDGenerator::generate();
$request = request();
if ($request) {
$sessionUuid = $request->input('uuid', $sessionUuid);
}
// 检查插件实体是否存在
$pluginDir = $this->plugin_path . DIRECTORY_SEPARATOR . $code;
$pluginConfigFile = $pluginDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'info.php';
if (!is_dir($pluginDir) || !is_file($pluginConfigFile)) {
yield Sse::error('插件不存在,无法卸载', [], $sessionUuid);
return;
}
// 检查插件是否允许卸载
$pluginConfig = $this->getPluginConfig($code);
if (isset($pluginConfig['uninstall']['undeletable']) && $pluginConfig['uninstall']['undeletable'] === true) {
yield Sse::error('该插件为系统内置插件,不允许卸载', [], $sessionUuid);
return;
}
$installedConfigPath = $pluginDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'installed.php';
if (!is_file($installedConfigPath)) {
yield Sse::error('插件已卸载,无需重复执行卸载操作', [], $sessionUuid);
return;
}
// 检查插件是否提供 Install.php
$className = "plugin\\{$code}\\Install";
if (!class_exists($className)) {
yield Sse::error("插件未提供 Install.php,无法进行卸载", [], $sessionUuid);
return;
}
try {
// 发送卸载开始事件
yield Sse::progress('开始卸载插件', 10, ['plugin' => $code], $sessionUuid);
// 统一调用插件的 Install::uninstall() 方法(这是唯一的卸载入口)
yield Sse::progress('执行插件卸载逻辑', 30, [], $sessionUuid);
$version = $pluginConfig['version'] ?? '1.0.0';
// 实例化并执行插件 Install 类(传入进度回调以支持在线模式反馈)
$installInstance = new $className();
// 创建临时输出文件(类似 Terminal 的方式)
$outputFile = runtime_path(self::RUNTIME_PLUGIN_PATH . '/uninstall_' . $sessionUuid . '.log');
// 确保目录存在
$outputDir = dirname($outputFile);
if (!is_dir($outputDir)) {
mkdir($outputDir, 0777, true);
}
file_put_contents($outputFile, '');
// 定义进度回调,将插件的卸载进度实时写入文件
$callbackCount = 0;
$progressCallback = function (string $message, ?int $progress = null) use ($outputFile, &$callbackCount) {
$callbackCount++;
$progressStr = $progress !== null ? "(progress: {$progress})" : "(no progress)";
$logLine = "[CALLBACK #{$callbackCount}] {$message} {$progressStr}\n";
// 写入文件(追加模式)
file_put_contents($outputFile, $logLine, FILE_APPEND);
};
// 设置进度回调和在线模式
$installInstance->setProgressCallback($progressCallback);
$installInstance->setOnlineMode(true);
// 开启输出缓冲,捕获 Install::uninstall() 内部的 echo 输出
ob_start();
$installInstance->uninstall($version);
$output = ob_get_clean();
// 将缓冲区输出也写入临时文件
if (!empty($output)) {
file_put_contents($outputFile, $output, FILE_APPEND);
}
// 读取文件内容并 yield 所有日志
$fileContent = file_get_contents($outputFile);
if (!empty($fileContent)) {
$lines = array_filter(explode("\n", $fileContent));
foreach ($lines as $line) {
$line = trim($line);
if (!empty($line)) {
// 使用 Sse::progress 统一输出,不更新进度值
yield Sse::progress($line, 30, [], $sessionUuid);
}
}
}
// 清理临时文件
@unlink($outputFile);
yield Sse::progress('插件卸载方法执行成功', 80, [], $sessionUuid);
// 删除插件内的安装配置
yield Sse::progress('删除安装配置', 90, [], $sessionUuid);
if (is_file($installedConfigPath)) {
@unlink($installedConfigPath);
}
// 发送卸载完成事件
yield Sse::completed('插件卸载成功', ['plugin' => $code], $sessionUuid);
} catch (\Throwable $e) {
// 发送卸载失败事件
yield Sse::error('卸载失败:' . $e->getMessage(), [
'exception' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
], $sessionUuid);
}
}
/**
* 删除插件(仅删除插件包)
* 删除条件:
* 1. 插件必须为已卸载状态(installed.php 不存在)
* 2. 非官方插件(undeletable != true)
*
* @param string $code 插件编码
*
* @return void 删除成功返回 true
* @throws \Exception
*/
public function delete(string $code): void
{
// 1. 检查插件实体是否存在
$pluginDir = $this->plugin_path . DIRECTORY_SEPARATOR . $code;
$pluginConfigFile = $pluginDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'info.php';
if (!is_dir($pluginDir) || !is_file($pluginConfigFile)) {
throw new \Exception('插件不存在,无法删除');
}
// 2. 检查插件配置
$pluginConfig = $this->getPluginConfig($code);
// 3. 检查是否为系统内置插件(不允许删除)
if (isset($pluginConfig['uninstall']['undeletable']) && $pluginConfig['uninstall']['undeletable'] === true) {
throw new \Exception('系统内置插件不允许删除');
}
// 4. 检查插件安装状态(installed.php 不存在)
$installedConfigPath = $pluginDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'installed.php';
$isInstalled = is_file($installedConfigPath);
if ($isInstalled) {
throw new \Exception('插件已安装,请先卸载后再删除');
}
// 5. 检查插件是否提供 Install.php
$className = "plugin\\{$code}\\Install";
if (!class_exists($className)) {
throw new \Exception("插件未提供 Install.php,无法进行删除");
}
try {
$version = $pluginConfig['version'] ?? '1.0.0';
// 实例化并执行插件 Install 类
$installInstance = new $className();
// 设置为 CLI 模式(不使用回调)
$installInstance->setOnlineMode(false);
// 执行删除
$installInstance->delete($version);
// 删除后,需要清理数据库中的插件记录
$this->deletePluginRecord($code);
} catch (\Throwable $e) {
throw new \Exception('删除失败:' . $e->getMessage());
}
}
/**
* 删除插件数据库记录
*/
protected function deletePluginRecord(string $code): void
{
try {
/** @var PluginService $service */
$service = Container::make(PluginService::class);
$model = $service->dao->query()->where('key', $code)->first();
if ($model) {
$model->delete();
}
} catch (\Throwable $e) {
throw new \RuntimeException('删除插件记录失败:' . $e->getMessage(), 0, $e);
}
}
}