// +---------------------------------------------------------------------- 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']); } } } }