PHP项目插件机制与热加载

wen PHP项目 3

PHP项目插件机制与热加载实现

插件机制设计

插件接口定义

<?php
// PluginInterface.php
interface PluginInterface {
    public function getName(): string;
    public function getVersion(): string;
    public function initialize(): void;
    public function activate(): void;
    public function deactivate(): void;
    public function getHooks(): array;
}
// Hookable.php
interface Hookable {
    public function execute(string $hook, ...$args);
    public function addHook(string $hook, callable $callback, int $priority = 10);
    public function removeHook(string $hook, callable $callback);
}

插件管理器核心类

<?php
// PluginManager.php
class PluginManager implements Hookable {
    private static ?PluginManager $instance = null;
    private array $plugins = [];
    private array $hooks = [];
    private string $pluginDir;
    private string $cacheDir;
    private bool $hotReloadEnabled = false;
    private function __construct(string $pluginDir, string $cacheDir) {
        $this->pluginDir = $pluginDir;
        $this->cacheDir = $cacheDir;
        $this->initializeAutoloader();
    }
    public static function getInstance(string $pluginDir = '', string $cacheDir = ''): self {
        if (self::$instance === null) {
            self::$instance = new self($pluginDir ?: __DIR__ . '/plugins', 
                                        $cacheDir ?: __DIR__ . '/cache');
        }
        return self::$instance;
    }
    private function initializeAutoloader(): void {
        spl_autoload_register(function ($class) {
            // 检查插件目录
            foreach ($this->plugins as $plugin) {
                $file = $plugin['dir'] . '/' . str_replace('\\', '/', $class) . '.php';
                if (file_exists($file)) {
                    require_once $file;
                    return true;
                }
            }
            return false;
        });
    }
    // 扫描并加载所有插件
    public function loadAllPlugins(): void {
        $directories = glob($this->pluginDir . '/*', GLOB_ONLYDIR);
        foreach ($directories as $dir) {
            $this->loadPlugin($dir);
        }
    }
    // 加载单个插件
    public function loadPlugin(string $dir): ?PluginInterface {
        $manifest = $dir . '/manifest.json';
        $mainFile = $dir . '/main.php';
        if (!file_exists($manifest) || !file_exists($mainFile)) {
            return null;
        }
        try {
            $config = json_decode(file_get_contents($manifest), true);
            if (!isset($config['main_class'])) {
                throw new \Exception("Missing main_class in manifest");
            }
            require_once $mainFile;
            $className = $config['main_class'];
            if (!class_exists($className)) {
                throw new \Exception("Class $className not found");
            }
            $plugin = new $className();
            if (!$plugin instanceof PluginInterface) {
                throw new \Exception("Plugin must implement PluginInterface");
            }
            $plugin->initialize();
            $this->plugins[$plugin->getName()] = [
                'instance' => $plugin,
                'dir' => $dir,
                'config' => $config,
                'loaded' => true
            ];
            // 注册插件钩子
            $this->registerPluginHooks($plugin);
            return $plugin;
        } catch (\Exception $e) {
            error_log("Failed to load plugin from $dir: " . $e->getMessage());
            return null;
        }
    }
    // 注册插件钩子
    private function registerPluginHooks(PluginInterface $plugin): void {
        foreach ($plugin->getHooks() as $hook => $callbacks) {
            foreach ($callbacks as $callback) {
                $this->addHook($hook, $callback[0], $callback[1] ?? 10);
            }
        }
    }
    // 执行钩子
    public function execute(string $hook, ...$args) {
        if (!isset($this->hooks[$hook])) {
            return $args[0] ?? null;
        }
        $result = $args[0] ?? null;
        foreach ($this->hooks[$hook] as $callback) {
            $result = call_user_func_array($callback, [$result, ...$args]);
        }
        return $result;
    }
    // 添加钩子
    public function addHook(string $hook, callable $callback, int $priority = 10): void {
        $this->hooks[$hook][$priority][] = $callback;
        ksort($this->hooks[$hook]); // 按优先级排序
    }
    // 移除钩子
    public function removeHook(string $hook, callable $callback): void {
        if (isset($this->hooks[$hook])) {
            foreach ($this->hooks[$hook] as $priority => &$callbacks) {
                $callbacks = array_filter($callbacks, function ($cb) use ($callback) {
                    return $cb !== $callback;
                });
            }
        }
    }
    // 启用热加载
    public function enableHotReload(): void {
        $this->hotReloadEnabled = true;
        if (php_sapi_name() === 'cli') {
            $this->enableCLIHotReload();
        } else {
            $this->enableWebHotReload();
        }
    }
    // CLI模式热加载
    private function enableCLIHotReload(): void {
        $this->initialFileHash = $this->getPluginDirectoryHash();
        while (true) {
            usleep(500000); // 0.5秒检查一次
            $currentHash = $this->getPluginDirectoryHash();
            if ($currentHash !== $this->initialFileHash) {
                echo "Detected plugin changes, reloading...\n";
                $this->reloadAllPlugins();
                $this->initialFileHash = $currentHash;
            }
        }
    }
    // Web模式热加载
    private function enableWebHotReload(): void {
        // 使用文件修改时间检查
        $cacheKey = 'plugin_hash_' . md5($this->pluginDir);
        $lastHash = $this->getCache($cacheKey);
        $currentHash = $this->getPluginDirectoryHash();
        if ($lastHash !== $currentHash) {
            $this->reloadAllPlugins();
            $this->setCache($cacheKey, $currentHash);
        }
    }
    // 重新加载所有插件
    public function reloadAllPlugins(): void {
        // 停用所有插件
        foreach ($this->plugins as $name => $pluginInfo) {
            $pluginInfo['instance']->deactivate();
        }
        // 清空插件和钩子
        $this->plugins = [];
        $this->hooks = [];
        // 重新加载
        $this->loadAllPlugins();
    }
    // 重新加载单个插件
    public function reloadPlugin(string $name): bool {
        if (isset($this->plugins[$name])) {
            // 停用并移除
            $this->plugins[$name]['instance']->deactivate();
            unset($this->plugins[$name]);
            // 重新加载
            $dir = $this->plugins[$name]['dir'] ?? $this->pluginDir . '/' . $name;
            return $this->loadPlugin($dir) !== null;
        }
        return false;
    }
    // 获取插件目录哈希
    private function getPluginDirectoryHash(): string {
        $hash = '';
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($this->pluginDir)
        );
        foreach ($iterator as $file) {
            if ($file->isFile() && $file->getExtension() === 'php') {
                $hash .= md5_file($file->getRealPath());
            }
        }
        return md5($hash);
    }
    // 缓存操作
    private function getCache(string $key): ?string {
        $file = $this->cacheDir . '/' . $key;
        if (file_exists($file)) {
            return file_get_contents($file);
        }
        return null;
    }
    private function setCache(string $key, string $value): void {
        if (!is_dir($this->cacheDir)) {
            mkdir($this->cacheDir, 0755, true);
        }
        file_put_contents($this->cacheDir . '/' . $key, $value);
    }
    // 获取所有插件信息
    public function getPlugins(): array {
        return $this->plugins;
    }
    // 私有化克隆方法
    private function __clone() {}
    private function __wakeup() {}
}

示例插件实现

示例插件:日志记录插件

<?php
// plugins/logger/manifest.json
/*
{
    "name": "logger",
    "version": "1.0.0",
    "main_class": "LoggerPlugin",
    "description": "Logs all application events"
}
*/
// plugins/logger/main.php
class LoggerPlugin implements PluginInterface {
    private string $logFile;
    public function getName(): string {
        return 'Logger Plugin';
    }
    public function getVersion(): string {
        return '1.0.0';
    }
    public function initialize(): void {
        $this->logFile = __DIR__ . '/logs/app.log';
        if (!is_dir(dirname($this->logFile))) {
            mkdir(dirname($this->logFile), 0755, true);
        }
    }
    public function activate(): void {
        $pluginManager = PluginManager::getInstance();
        // 注册钩子
        $pluginManager->addHook('after_request', [$this, 'logRequest'], 10);
        $pluginManager->addHook('after_error', [$this, 'logError'], 20);
    }
    public function deactivate(): void {
        $pluginManager = PluginManager::getInstance();
        $pluginManager->removeHook('after_request', [$this, 'logRequest']);
        $pluginManager->removeHook('after_error', [$this, 'logError']);
    }
    public function getHooks(): array {
        return [
            'after_request' => [[[$this, 'logRequest'], 10]],
            'after_error' => [[[$this, 'logError'], 20]]
        ];
    }
    public function logRequest($response, $request = null) {
        $logEntry = sprintf(
            "[%s] %s %s\n",
            date('Y-m-d H:i:s'),
            $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN',
            $_SERVER['REQUEST_URI'] ?? 'UNKNOWN'
        );
        file_put_contents($this->logFile, $logEntry, FILE_APPEND);
        return $response;
    }
    public function logError($error) {
        $logEntry = sprintf(
            "[%s] ERROR: %s\n",
            date('Y-m-d H:i:s'),
            $error
        );
        file_put_contents($this->logFile, $logEntry, FILE_APPEND);
        return $error;
    }
}

缓存插件示例

<?php
// plugins/cache/manifest.json
/*
{
    "name": "cache",
    "version": "1.0.0",
    "main_class": "CachePlugin",
    "description": "Response caching plugin"
}
*/
// plugins/cache/main.php
class CachePlugin implements PluginInterface {
    private array $cache = [];
    private int $ttl = 3600;
    public function getName(): string {
        return 'Cache Plugin';
    }
    public function getVersion(): string {
        return '1.0.0';
    }
    public function initialize(): void {
        // 初始化缓存
    }
    public function activate(): void {
        $pluginManager = PluginManager::getInstance();
        // 高优先级缓存钩子
        $pluginManager->addHook('before_response', [$this, 'getCached'], 1);
        $pluginManager->addHook('after_response', [$this, 'setCached'], 100);
    }
    public function deactivate(): void {
        $pluginManager = PluginManager::getInstance();
        $pluginManager->removeHook('before_response', [$this, 'getCached']);
        $pluginManager->removeHook('after_response', [$this, 'setCached']);
        $this->clearCache();
    }
    public function getHooks(): array {
        return [
            'before_response' => [[[$this, 'getCached'], 1]],
            'after_response' => [[[$this, 'setCached'], 100]]
        ];
    }
    public function getCached($response, $request = null) {
        $key = $this->generateKey($request);
        if (isset($this->cache[$key]) && $this->cache[$key]['expires'] > time()) {
            return $this->cache[$key]['data'];
        }
        return $response;
    }
    public function setCached($response, $request = null) {
        $key = $this->generateKey($request);
        $this->cache[$key] = [
            'data' => $response,
            'expires' => time() + $this->ttl
        ];
        return $response;
    }
    private function generateKey($request): string {
        return md5($_SERVER['REQUEST_URI'] ?? '');
    }
    private function clearCache(): void {
        $this->cache = [];
    }
}

热加载实现细节

文件监控类

<?php
// FileWatcher.php
class FileWatcher {
    private string $directory;
    private array $fileHashes = [];
    private array $callbacks = [];
    public function __construct(string $directory) {
        $this->directory = $directory;
        $this->scanFiles();
    }
    public function watch(callable $callback): void {
        $this->callbacks[] = $callback;
    }
    public function checkForChanges(): bool {
        $currentHashes = $this->scanFiles();
        if ($currentHashes !== $this->fileHashes) {
            $this->fileHashes = $currentHashes;
            $this->notifyCallbacks();
            return true;
        }
        return false;
    }
    private function scanFiles(): array {
        $hashes = [];
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($this->directory)
        );
        foreach ($iterator as $file) {
            if ($file->isFile() && in_array($file->getExtension(), ['php', 'json'])) {
                $hashes[$file->getRealPath()] = [
                    'md5' => md5_file($file->getRealPath()),
                    'mtime' => $file->getMTime()
                ];
            }
        }
        return $hashes;
    }
    private function notifyCallbacks(): void {
        foreach ($this->callbacks as $callback) {
            call_user_func($callback, $this->fileHashes);
        }
    }
}

热加载整合

<?php
// HotReloadIntegration.php
class HotReloadIntegration {
    private PluginManager $pluginManager;
    private FileWatcher $fileWatcher;
    private bool $running = false;
    public function __construct(PluginManager $pluginManager) {
        $this->pluginManager = $pluginManager;
        $this->fileWatcher = new FileWatcher($pluginManager->pluginDir);
    }
    public function start(): void {
        $this->running = true;
        $this->fileWatcher->watch(function ($hashes) {
            $this->handleChanges($hashes);
        });
        // 启动轮询
        while ($this->running) {
            $this->fileWatcher->checkForChanges();
            usleep(500000); // 0.5秒
        }
    }
    public function stop(): void {
        $this->running = false;
    }
    private function handleChanges(array $hashes): void {
        echo "Plugin changes detected, reloading...\n";
        // 获取更改的文件
        $changedFiles = $this->getChangedFiles($hashes);
        foreach ($changedFiles as $file) {
            $pluginName = $this->extractPluginName($file);
            if ($pluginName) {
                $this->pluginManager->reloadPlugin($pluginName);
            }
        }
    }
    private function getChangedFiles(array $currentHashes): array {
        // 比较当前和之前的哈希值
        // 实现省略...
        return [];
    }
    private function extractPluginName(string $filePath): ?string {
        // 从文件路径提取插件名称
        preg_match('/plugins\/([^\/]+)\//', $filePath, $matches);
        return $matches[1] ?? null;
    }
}

使用示例

基本用法

<?php
// index.php
require_once 'PluginManager.php';
require_once 'PluginInterface.php';
require_once 'FileWatcher.php';
// 初始化插件管理器
$pluginManager = PluginManager::getInstance(__DIR__ . '/plugins', __DIR__ . '/cache');
// 加载所有插件
$pluginManager->loadAllPlugins();
// 激活所有插件
foreach ($pluginManager->getPlugins() as $name => $pluginInfo) {
    $pluginInfo['instance']->activate();
}
// 启用热加载(可选)
$pluginManager->enableHotReload();
// 使用钩子系统
$response = $pluginManager->execute('before_response', $initialResponse);
// ... 处理请求 ...
$response = $pluginManager->execute('after_response', $finalResponse);
// 输出最终响应
echo $response;

CLI模式热加载

<?php
// hotreload.php
require_once 'PluginManager.php';
require_once 'HotReloadIntegration.php';
$pluginManager = PluginManager::getInstance();
$pluginManager->loadAllPlugins();
$hotReload = new HotReloadIntegration($pluginManager);
$hotReload->start(); // 开始监控

注意事项

  1. 性能考虑:文件监控会消耗系统资源,生产环境建议使用缓存
  2. 安全性:确保插件目录有适当权限,防止恶意插件注入
  3. 兼容性:不同PHP版本对匿名类、闭包等支持不同
  4. 依赖管理:插件可能需要特定的PHP扩展或库
  5. 错误处理:插件加载失败不应影响主应用运行

这个实现提供了完整的插件机制和热加载支持,可以根据实际需求进行扩展和优化。

PHP项目插件机制与热加载

抱歉,评论功能暂时关闭!