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(); // 开始监控
注意事项
- 性能考虑:文件监控会消耗系统资源,生产环境建议使用缓存
- 安全性:确保插件目录有适当权限,防止恶意插件注入
- 兼容性:不同PHP版本对匿名类、闭包等支持不同
- 依赖管理:插件可能需要特定的PHP扩展或库
- 错误处理:插件加载失败不应影响主应用运行
这个实现提供了完整的插件机制和热加载支持,可以根据实际需求进行扩展和优化。
