|null $directories Null = konfigurierte Verzeichnisse * @return array{scan_id: string, files_scanned: int, files_with_errors: int, total_classes: int, total_functions: int, duration_ms: int} */ public function scan(?array $directories = null, string $triggeredBy = 'web'): array { $start = hrtime(true); $scanId = $this->generateUuid(); $dirs = $directories ?? $this->getEnabledDirectories(); $items = []; $totalClasses = 0; $totalFunctions = 0; $filesWithErrors = 0; foreach ($dirs as $directory) { $config = $this->getConfigForDirectory($directory); $extensions = $this->parseExtensions($config['extensions'] ?? 'php'); $excludePatterns = $this->parseExcludePatterns($config['exclude_patterns'] ?? ''); $files = $this->scanDirectory($directory, $extensions, $excludePatterns); foreach ($files as $filePath) { $item = $this->analyzeFile($filePath, $triggeredBy); $items[] = $item; if ($item['parse_error'] !== null) { $filesWithErrors++; } $totalClasses += count($item['classes']); $totalFunctions += count($item['functions']); } } $this->repository->saveBatch($items, $scanId); $this->repository->deleteByNotScanId($scanId); $durationMs = (int) ((hrtime(true) - $start) / 1_000_000); return [ 'scan_id' => $scanId, 'files_scanned' => count($items), 'files_with_errors' => $filesWithErrors, 'total_classes' => $totalClasses, 'total_functions' => $totalFunctions, 'duration_ms' => $durationMs, ]; } /** * @return array */ private function getEnabledDirectories(): array { $configs = $this->repository->getConfiguredDirectories(); return array_column($configs, 'directory'); } /** * @return array */ private function getConfigForDirectory(string $directory): array { $configs = $this->repository->getConfiguredDirectories(); foreach ($configs as $config) { if ($config['directory'] === $directory) { return $config; } } return ['extensions' => 'php', 'exclude_patterns' => '']; } /** * @return array */ private function parseExtensions(string $extensions): array { return array_map('trim', explode(',', $extensions)); } /** * @return array */ private function parseExcludePatterns(string $patterns): array { if ($patterns === '') { return []; } return array_map('trim', explode(',', $patterns)); } /** * @param array $extensions * @param array $excludePatterns * @return array */ private function scanDirectory(string $directory, array $extensions, array $excludePatterns): array { $files = []; if (!is_dir($directory)) { return $files; } $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS) ); foreach ($iterator as $file) { if (!$file->isFile()) { continue; } $filePath = $file->getPathname(); $ext = $file->getExtension(); if (!in_array($ext, $extensions, true)) { continue; } if ($this->isExcluded($filePath, $directory, $excludePatterns)) { continue; } $files[] = $filePath; } return $files; } /** * @param array $patterns */ private function isExcluded(string $filePath, string $rootDir, array $patterns): bool { $relativePath = str_replace($rootDir . '/', '', $filePath); foreach ($patterns as $pattern) { if (fnmatch($pattern, $relativePath)) { return true; } } return false; } /** * @return array */ private function analyzeFile(string $filePath, string $triggeredBy): array { $extension = pathinfo($filePath, PATHINFO_EXTENSION); $parsed = $this->getParserForExtension($extension)->parse($filePath); $stat = stat($filePath); return [ 'file_path' => $filePath, 'file_name' => basename($filePath), 'extension' => pathinfo($filePath, PATHINFO_EXTENSION), 'directory' => dirname($filePath), 'file_size' => $stat ? $stat['size'] : 0, 'line_count' => $this->countLines($filePath), 'modified_at' => $stat ? date('Y-m-d H:i:s', $stat['mtime']) : date('Y-m-d H:i:s'), 'namespace' => $parsed['namespace'], 'classes' => $parsed['classes'], 'functions' => $parsed['functions'], 'uses' => $parsed['uses'], 'extends_class' => $parsed['extends_class'], 'implements_interfaces' => $parsed['implements_interfaces'], 'traits_used' => $parsed['traits_used'], 'constructor_deps' => $parsed['constructor_deps'], 'parse_error' => $parsed['error'], 'triggered_by' => $triggeredBy, ]; } private function countLines(string $filePath): int { $handle = fopen($filePath, 'r'); if ($handle === false) { return 0; } $count = 0; while (!feof($handle)) { fgets($handle); $count++; } fclose($handle); return $count; } private function generateUuid(): string { return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0x0fff) | 0x4000, random_int(0, 0x3fff) | 0x8000, random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff) ); } }