343 lines
10 KiB
PHP
343 lines
10 KiB
PHP
<?php
|
|
// +----------------------------------------------------------------------
|
|
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
|
// +----------------------------------------------------------------------
|
|
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
|
// +----------------------------------------------------------------------
|
|
// | Author: ywxapp<admin@ywxapp.cn>
|
|
// +----------------------------------------------------------------------
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace ywxapp\service;
|
|
|
|
use think\Exception;
|
|
use think\facade\Cache;
|
|
use think\facade\Config;
|
|
|
|
/**
|
|
* 插件热重载服务
|
|
*
|
|
* 功能特性:
|
|
* - 开发环境下的插件热重载
|
|
* - 无需重启即可加载插件修改
|
|
* - 自动检测文件变更
|
|
* - 支持选择性重载
|
|
*/
|
|
class AddonHotReload
|
|
{
|
|
/** 热重载状态缓存键 */
|
|
const RELOAD_STATUS_KEY = 'addon_hot_reload_status';
|
|
|
|
/** 文件变更检测间隔(秒) */
|
|
const CHECK_INTERVAL = 2;
|
|
|
|
/**
|
|
* 检查是否为开发环境
|
|
*
|
|
* @return bool
|
|
*/
|
|
private static function isDevelopmentEnvironment(): bool
|
|
{
|
|
return config('app.app_debug') === true;
|
|
}
|
|
|
|
/**
|
|
* 热重载指定插件
|
|
*
|
|
* @param string $addon 插件名称
|
|
* @param bool $force 是否强制重载
|
|
* @return bool
|
|
* @throws Exception
|
|
*/
|
|
public static function reloadAddon(string $addon, bool $force = false): bool
|
|
{
|
|
if (!self::isDevelopmentEnvironment()) {
|
|
throw new Exception('热重载功能仅开发环境可用');
|
|
}
|
|
|
|
// 检查插件是否存在
|
|
$addonPath = ADDON_PATH . $addon;
|
|
if (!is_dir($addonPath)) {
|
|
throw new Exception("插件不存在:{$addon}");
|
|
}
|
|
|
|
// 检查是否有文件变更
|
|
if (!$force && !self::hasFileChanges($addon)) {
|
|
return false; // 没有变更,无需重载
|
|
}
|
|
|
|
try {
|
|
// 1. 清除插件相关缓存
|
|
self::clearAddonCaches($addon);
|
|
// 2. 重新加载插件配置
|
|
$infoFile = $addonPath . DIRECTORY_SEPARATOR . 'info.php';
|
|
if (is_file($infoFile)) {
|
|
$info = include $infoFile;
|
|
// 3. 重新注册插件服务
|
|
if (isset($info['services']) && !empty($info['services'])) {
|
|
$services = $info['services'];
|
|
if (array_keys($services) === range(0, count($services) - 1)) {
|
|
$services = array_combine($services, $services);
|
|
}
|
|
app()->bind($services);
|
|
}
|
|
// 4. 重新加载事件监听
|
|
if (isset($info['events']) && !empty($info['events'])) {
|
|
app()->loadEvent($info['events']);
|
|
}
|
|
// 5. 重新加载中间件
|
|
if (isset($info['middleware']) && !empty($info['middleware'])) {
|
|
app()->middleware->import($info['middleware'], 'app');
|
|
}
|
|
}
|
|
// 6. 更新重载状态
|
|
self::updateReloadStatus($addon, [
|
|
'reload_time' => time(),
|
|
'status' => 'success',
|
|
'files_changed' => self::getChangedFiles($addon)
|
|
]);
|
|
|
|
\think\facade\Log::info("插件 {$addon} 热重载成功");
|
|
return true;
|
|
|
|
} catch (\Exception $e) {
|
|
self::updateReloadStatus($addon, [
|
|
'reload_time' => time(),
|
|
'status' => 'failed',
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
\think\facade\Log::error("插件 {$addon} 热重载失败:" . $e->getMessage());
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 热重载所有已启用插件
|
|
*
|
|
* @return array 重载结果
|
|
*/
|
|
public static function reloadAlladdon(): array
|
|
{
|
|
if (!self::isDevelopmentEnvironment()) {
|
|
throw new Exception('热重载功能仅开发环境可用');
|
|
}
|
|
|
|
$results = [];
|
|
$addon = Config::get('addon', []);
|
|
|
|
foreach ($addon as $addon) {
|
|
if (!isset($addon['name']) || !($addon['state'] ?? false)) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$reloaded = self::reloadAddon($addon['name']);
|
|
$results[$addon['name']] = [
|
|
'success' => true,
|
|
'reloaded' => $reloaded
|
|
];
|
|
} catch (\Exception $e) {
|
|
$results[$addon['name']] = [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* 检查插件是否有文件变更
|
|
*
|
|
* @param string $addon 插件名称
|
|
* @return bool
|
|
*/
|
|
private static function hasFileChanges(string $addon): bool
|
|
{
|
|
$addonPath = ADDON_PATH . $addon;
|
|
$lastCheckKey = 'addon_file_check_' . $addon;
|
|
$lastCheckTime = Cache::get($lastCheckKey, 0);
|
|
|
|
// 检查间隔限制
|
|
if (time() - $lastCheckTime < self::CHECK_INTERVAL) {
|
|
return false;
|
|
}
|
|
|
|
$currentHash = self::calculateDirectoryHash($addonPath);
|
|
$lastHashKey = 'addon_file_hash_' . $addon;
|
|
$lastHash = Cache::get($lastHashKey, '');
|
|
|
|
// 更新检查时间
|
|
Cache::set($lastCheckKey, time(), 60);
|
|
|
|
if ($currentHash !== $lastHash) {
|
|
Cache::set($lastHashKey, $currentHash, 3600);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 计算目录哈希值
|
|
*
|
|
* @param string $directory 目录路径
|
|
* @return string
|
|
*/
|
|
private static function calculateDirectoryHash(string $directory): string
|
|
{
|
|
$hash = '';
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS),
|
|
\RecursiveIteratorIterator::SELF_FIRST
|
|
);
|
|
|
|
foreach ($iterator as $file) {
|
|
if ($file->isFile()) {
|
|
$hash .= md5_file($file->getPathname()) . $file->getMTime();
|
|
}
|
|
}
|
|
|
|
return md5($hash);
|
|
}
|
|
|
|
/**
|
|
* 获取变更的文件列表
|
|
*
|
|
* @param string $addon 插件名称
|
|
* @return array
|
|
*/
|
|
private static function getChangedFiles(string $addon): array
|
|
{
|
|
// 这里简化处理,实际可以记录具体的文件变更
|
|
return ['all_files']; // 表示所有文件都可能已变更
|
|
}
|
|
|
|
/**
|
|
* 清除插件相关缓存
|
|
*
|
|
* @param string $addon 插件名称
|
|
*/
|
|
private static function clearAddonCaches(string $addon): void
|
|
{
|
|
// 清除配置缓存
|
|
Cache::delete('addon_config_' . $addon);
|
|
Cache::delete('addon_config_version_' . $addon);
|
|
|
|
// 清除插件加载缓存
|
|
AppService::clearAddonCache();
|
|
|
|
// 清除性能监控数据
|
|
AddonPerformanceMonitor::clearPerformanceData($addon);
|
|
}
|
|
|
|
/**
|
|
* 更新重载状态
|
|
*
|
|
* @param string $addon 插件名称
|
|
* @param array $status 状态信息
|
|
*/
|
|
private static function updateReloadStatus(string $addon, array $status): void
|
|
{
|
|
$statusKey = self::RELOAD_STATUS_KEY . '_' . $addon;
|
|
Cache::set($statusKey, $status, 3600);
|
|
}
|
|
|
|
/**
|
|
* 获取插件重载状态
|
|
*
|
|
* @param string $addon 插件名称
|
|
* @return array
|
|
*/
|
|
public static function getReloadStatus(string $addon): array
|
|
{
|
|
$statusKey = self::RELOAD_STATUS_KEY . '_' . $addon;
|
|
return Cache::get($statusKey, [
|
|
'status' => 'never_reloaded',
|
|
'reload_time' => null
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 启用自动热重载监听
|
|
*
|
|
* 注意:这需要在适当的地方调用,比如在中间件中
|
|
*/
|
|
public static function enableAutoReload(): void
|
|
{
|
|
if (!self::isDevelopmentEnvironment()) {
|
|
return;
|
|
}
|
|
|
|
// 检查是否有插件需要重载
|
|
$addon = Config::get('addon', []);
|
|
foreach ($addon as $addon) {
|
|
if (!isset($addon['name']) || !($addon['state'] ?? false)) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
if (self::hasFileChanges($addon['name'])) {
|
|
self::reloadAddon($addon['name']);
|
|
}
|
|
} catch (\Exception $e) {
|
|
// 静默处理错误,避免影响正常请求
|
|
\think\facade\Log::warning("自动热重载失败:{$addon['name']} - " . $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取热重载统计信息
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function getReloadStatistics(): array
|
|
{
|
|
$stats = [
|
|
'total_reloads' => 0,
|
|
'successful_reloads' => 0,
|
|
'failed_reloads' => 0,
|
|
'addon' => []
|
|
];
|
|
|
|
$addon = Config::get('addon', []);
|
|
foreach ($addon as $addon) {
|
|
if (!isset($addon['name'])) {
|
|
continue;
|
|
}
|
|
|
|
$status = self::getReloadStatus($addon['name']);
|
|
$stats['addon'][$addon['name']] = $status;
|
|
|
|
if ($status['status'] !== 'never_reloaded') {
|
|
$stats['total_reloads']++;
|
|
if ($status['status'] === 'success') {
|
|
$stats['successful_reloads']++;
|
|
} else {
|
|
$stats['failed_reloads']++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $stats;
|
|
}
|
|
|
|
/**
|
|
* 清除所有热重载状态
|
|
*/
|
|
public static function clearAllReloadStatus(): void
|
|
{
|
|
$addon = Config::get('addon', []);
|
|
foreach ($addon as $addon) {
|
|
if (isset($addon['name'])) {
|
|
Cache::delete(self::RELOAD_STATUS_KEY . '_' . $addon['name']);
|
|
Cache::delete('addon_file_check_' . $addon['name']);
|
|
Cache::delete('addon_file_hash_' . $addon['name']);
|
|
}
|
|
}
|
|
}
|
|
} |