['/vendor/', '/tests/', '/cache/', '/node_modules/'], 'skip_directories' => [], 'extensions' => ['php'], 'max_depth' => 20, 'follow_links' => false, 'cache_enabled' => true, 'cache_ttl' => 3600, 'debug' => false, ]; /** * 设置扫描器配置 */ public function setConfig(array $config): void { $this->config = array_merge($this->config, $config); } /** * 主扫描方法,支持缓存 */ public function scan(string $path, ?string $cacheFile = null): void { if ($this->config['cache_enabled']) { $cacheFile = $cacheFile ?? $this->getDefaultCacheFile(); if ($this->loadFromCache($cacheFile, $path)) { $this->processCachedFiles(); return; } } $this->scanDirectory($path); if ($this->config['cache_enabled']) { $this->saveToCache($cacheFile); } } /** * 扫描目录(替换原有的load_directory) */ private function scanDirectory(string $path, int $depth = 0): void { if ($depth > $this->config['max_depth']) { return; } try { $dir = new DirectoryIterator($path); } catch (Throwable $e) { $this->logError($e, ['path' => $path, 'action' => 'open_directory']); return; } foreach ($dir as $item) { if ($this->shouldSkipItem($item)) { continue; } $realPath = $item->getRealPath(); if ($item->isDir()) { if ($this->shouldSkipDirectory($realPath)) { continue; } // 如果是符号链接且不跟随链接,则跳过 if ($item->isLink() && !$this->config['follow_links']) { continue; } $this->scanDirectory($realPath, $depth + 1); } elseif ($item->isFile()) { $this->processFile($realPath); } } } /** * 判断是否应该跳过该目录项 */ private function shouldSkipItem(DirectoryIterator $item): bool { return $item->isDot() || str_starts_with($item->getFilename(), '.'); } /** * 判断是否应该跳过目录 */ private function shouldSkipDirectory(string $path): bool { $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; // 检查配置文件中的跳过目录 $skipDirs = array_merge( $this->config['skip_directories'], config('site.scanner.skip', []) ); if (in_array($path, $skipDirs)) { return true; } // 检查跳过模式 foreach ($this->config['skip_patterns'] as $pattern) { if (strpos($path, $pattern) !== false) { return true; } } return false; } /** * 处理单个文件 */ private function processFile(string $path): void { // 检查文件扩展名 if (!in_array(pathinfo($path, PATHINFO_EXTENSION), $this->config['extensions'])) { return; } // 检查是否已处理 if (in_array($path, $this->files)) { return; } // 检查文件是否需要重新处理(基于修改时间) if (!$this->shouldProcessFile($path)) { return; } $this->files[] = $path; try { $this->loadAndParseFile($path); } catch (Throwable $e) { $this->logError($e, [ 'file' => $path, 'action' => 'process_file' ]); // 如果是语法错误,可以选择抛出或记录 if ($e instanceof \ParseError) { throw $e; } } } /** * 判断文件是否需要处理 */ private function shouldProcessFile(string $path): bool { if (!file_exists($path)) { return false; } $mtime = filemtime($path); // 如果没有缓存过修改时间,或者文件已更新,则需要处理 if (!isset($this->fileMtimes[$path]) || $this->fileMtimes[$path] < $mtime) { $this->fileMtimes[$path] = $mtime; return true; } return false; } /** * 加载并解析文件 */ private function loadAndParseFile(string $path): void { // OPcache优化 $this->optimizeWithOpcache($path); // 加载文件 require_once $path; // 尝试从文件内容提取类名(更准确) $class = $this->extractClassNameFromFile($path); if ($class && class_exists($class)) { $this->analyzeClass($class); } } /** * 使用OPcache优化文件 */ private function optimizeWithOpcache(string $path): void { if ($this->hasOpcache === null) { $this->hasOpcache = function_exists('opcache_invalidate') && function_exists('opcache_compile_file'); } if ($this->hasOpcache) { try { opcache_invalidate($path, true); opcache_compile_file($path); } catch (Throwable $e) { // 静默处理OPcache错误,不影响主要功能 if ($this->config['debug']) { $this->logError($e, [ 'file' => $path, 'action' => 'opcache_optimize' ]); } } } } /** * 从文件内容提取完整的类名(命名空间+类名) */ private function extractClassNameFromFile(string $path): ?string { $content = @file_get_contents($path); if (!$content) { return null; } $tokens = token_get_all($content); $namespace = ''; $class = ''; $classToken = false; foreach ($tokens as $token) { if (is_array($token)) { if ($token[0] === T_NAMESPACE) { $namespace = ''; $collecting = true; } elseif ($collecting && $token[0] === T_STRING) { $namespace .= $token[1]; } elseif ($collecting && $token[0] === T_NS_SEPARATOR) { $namespace .= '\\'; } elseif ($collecting && $token[0] === T_WHITESPACE) { // 忽略空白 } else { $collecting = false; } if ($token[0] === T_CLASS || $token[0] === T_INTERFACE || $token[0] === T_TRAIT) { $classToken = true; } elseif ($classToken && $token[0] === T_STRING) { $class = $token[1]; break; } } else { if ($token === ';' || $token === '{') { $collecting = false; } } } if (!$class) { return null; } return $namespace ? $namespace . '\\' . $class : $class; } /** * 分析类及其方法 */ private function analyzeClass(string $class): void { try { $reflect = $this->container->getReflectionClass($class); if (!$reflect->isInstantiable() || $reflect->isTrait() || $reflect->isEnum() || $reflect->isInterface()) { return; } // 检查跳过注解 if ($this->shouldSkipClass($reflect)) { return; } // 分析类的方法 $this->analyzeClassMethods($reflect, $class); } catch (Throwable $e) { $this->logError($e, [ 'class' => $class, 'action' => 'analyze_class' ]); } } /** * 判断是否应该跳过该类 */ private function shouldSkipClass(ReflectionClass $reflect): bool { $attributes = array_map( fn($attr) => $attr->getName(), $reflect->getAttributes() ); return in_array(Skip::class, $attributes, true) || in_array(\Attribute::class, $attributes, true); } /** * 分析类的方法 */ private function analyzeClassMethods(ReflectionClass $reflect, string $class): void { foreach ($reflect->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { // 跳过静态方法和继承的方法 if ($method->isStatic() || $method->getDeclaringClass()->getName() !== $class) { continue; } $this->processMethodAttributes($method, $class); } } /** * 处理方法上的注解 */ private function processMethodAttributes(ReflectionMethod $method, string $class): void { foreach ($method->getAttributes() as $attribute) { $attributeName = $attribute->getName(); if (!class_exists($attributeName)) { continue; } try { $instance = $attribute->newInstance(); if (method_exists($instance, 'dispatch')) { $instance->dispatch($class, $method->getName()); } } catch (Throwable $e) { $this->logError($e, [ 'class' => $class, 'method' => $method->getName(), 'attribute' => $attributeName, 'action' => 'process_attribute' ]); } } } /** * 缓存相关方法 */ private function getDefaultCacheFile(): string { $cacheDir = sys_get_temp_dir() . '/kiri-scanner-cache/'; if (!is_dir($cacheDir)) { mkdir($cacheDir, 0755, true); } $projectHash = md5(__DIR__); return $cacheDir . 'scanner_' . $projectHash . '.json'; } /** * 从缓存加载 */ private function loadFromCache(string $cacheFile, string $path): bool { if (!file_exists($cacheFile)) { return false; } // 检查缓存是否过期 if (filemtime($cacheFile) + $this->config['cache_ttl'] < time()) { return false; } // 检查目录是否有更新 if ($this->hasDirectoryChanged($path, $cacheFile)) { return false; } $data = json_decode(file_get_contents($cacheFile), true); if (!is_array($data) || !isset($data['files'], $data['mtimes'])) { return false; } $this->files = $data['files']; $this->fileMtimes = $data['mtimes']; return true; } /** * 检查目录是否有更新 */ private function hasDirectoryChanged(string $path, string $cacheFile): bool { $cacheMtime = filemtime($cacheFile); // 简单的实现:检查目录本身和一级子目录 $dirIterator = new DirectoryIterator($path); foreach ($dirIterator as $item) { if ($item->isDot()) { continue; } $itemPath = $item->getRealPath(); if (filemtime($itemPath) > $cacheMtime) { return true; } } return false; } /** * 保存到缓存 */ private function saveToCache(string $cacheFile): void { $data = [ 'files' => $this->files, 'mtimes' => $this->fileMtimes, 'timestamp' => time(), 'version' => '1.0' ]; file_put_contents($cacheFile, json_encode($data, JSON_PRETTY_PRINT)); // 清理旧缓存文件 $this->cleanupOldCacheFiles(dirname($cacheFile)); } /** * 清理旧的缓存文件 */ private function cleanupOldCacheFiles(string $cacheDir): void { $files = glob($cacheDir . '/scanner_*.json'); $now = time(); foreach ($files as $file) { if (filemtime($file) + (7 * 24 * 3600) < $now) { // 清理7天前的缓存 unlink($file); } } } /** * 处理缓存的文件(批量处理,避免重复加载) */ private function processCachedFiles(): void { foreach ($this->files as $file) { if ($this->shouldProcessFile($file)) { $this->loadAndParseFile($file); } } } /** * 错误记录方法 */ private function logError(Throwable $e, array $context = []): void { json_log($e, $context); } /** * 重置扫描器状态 */ public function reset(): void { $this->files = []; $this->fileMtimes = []; $this->hasOpcache = null; } }