// +---------------------------------------------------------------------- declare(strict_types=1); namespace crmeb\services; use crmeb\plugin\PluginManifest; /** * 插件隔离缓存服务 * * 根据调用方所在插件目录读取 plugin.json,自动使用插件 canonical id 作为 * key 命名空间和独立 tag,防止插件缓存互相覆盖或批量清理全站缓存。 */ class PluginCacheService { /** @var array */ protected static array $scopeCache = []; /** * 写入插件缓存 * * @param string $name 缓存名称 * @param mixed $value 缓存值 * @param int $expire 缓存时间 * @return mixed */ public static function set(string $name, $value, int $expire = 0) { $scope = self::resolveScope(); return CacheService::set($scope['prefix'] . self::normalizeName($name), $value, $expire, $scope['tag']); } /** * 插件缓存不存在时写入 * * @param string $name 缓存名称 * @param mixed $default 默认值或回源回调 * @param int $expire 缓存时间 * @return mixed */ public static function remember(string $name, $default = '', int $expire = 0) { $scope = self::resolveScope(); return CacheService::remember( $scope['prefix'] . self::normalizeName($name), $default, $expire, $scope['tag'] ); } /** * 读取插件缓存 * * @param string $name 缓存名称 * @param mixed $default 默认值 * @return mixed */ public static function get(string $name, $default = '') { $scope = self::resolveScope(); return CacheService::get($scope['prefix'] . self::normalizeName($name), $default); } /** * 判断插件缓存是否存在 * * @param string $name 缓存名称 * @return bool */ public static function has(string $name): bool { $scope = self::resolveScope(); return CacheService::has($scope['prefix'] . self::normalizeName($name)); } /** * 删除单个插件缓存 * * @param string $name 缓存名称 * @return bool */ public static function delete(string $name): bool { $scope = self::resolveScope(); return CacheService::delete($scope['prefix'] . self::normalizeName($name)); } /** * 清空当前插件的全部缓存 * * @return bool */ public static function clear(): bool { $scope = self::resolveScope(); return CacheService::clearPluginTag($scope['tag']); } /** * 从调用栈定位当前插件并生成隔离命名空间 * * @return array{prefix: string, tag: string} */ protected static function resolveScope(): array { foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { $file = (string)($frame['file'] ?? ''); if ($file === '') { continue; } $normalized = str_replace('\\', '/', $file); if (!str_contains($normalized, '/modules/') && !str_contains($normalized, '/driver/')) { continue; } if (isset(self::$scopeCache[$normalized])) { return self::$scopeCache[$normalized]; } [$manifestFile, $rootPath] = self::findManifest($file); $manifest = PluginManifest::fromFile($manifestFile, $rootPath); $pluginId = $manifest->identity->canonicalId(); return self::$scopeCache[$normalized] = [ 'prefix' => "plugin:{$pluginId}:", 'tag' => "plugin:{$pluginId}", ]; } throw new \LogicException('PluginCacheService 只能由包含有效 plugin.json 的插件代码调用'); } /** * 向上查找调用方所属插件清单 * * @param string $file 插件调用文件 * @return array{0: string, 1: string} */ protected static function findManifest(string $file): array { $directory = dirname($file); while ($directory !== dirname($directory)) { $manifestFile = $directory . DIRECTORY_SEPARATOR . 'plugin.json'; if (is_file($manifestFile)) { $rootPath = self::findBackendRoot($directory); return [$manifestFile, $rootPath]; } $directory = dirname($directory); } throw new \LogicException("插件调用文件不属于有效插件目录:{$file}"); } /** * 根据插件目录定位 backend 根目录 * * @param string $directory 插件目录 * @return string */ protected static function findBackendRoot(string $directory): string { $current = $directory; while ($current !== dirname($current)) { $name = basename($current); if ($name === 'modules' || $name === 'driver') { return rtrim(dirname($current), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; } $current = dirname($current); } throw new \LogicException("无法定位插件 backend 根目录:{$directory}"); } /** * 校验插件缓存名称 * * @param string $name 缓存名称 * @return string */ protected static function normalizeName(string $name): string { $name = trim($name); if ($name === '' || preg_match('/[\x00-\x1F\x7F]/', $name)) { throw new \InvalidArgumentException('插件缓存名称不能为空或包含控制字符'); } return $name; } }