Files

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