|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); // Quality-Analyse ausführen $qualityStats = $this->runQualityAnalysis($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, 'quality_issues' => $qualityStats['total_issues'], 'quality_avg_grade' => $qualityStats['avg_grade'], ]; } /** * @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); $fileContent = @file_get_contents($filePath) ?: ''; $lineCount = $fileContent !== '' ? substr_count($fileContent, "\n") + 1 : 0; return [ 'file_path' => $filePath, 'file_name' => basename($filePath), 'extension' => pathinfo($filePath, PATHINFO_EXTENSION), 'directory' => dirname($filePath), 'file_size' => $stat ? $stat['size'] : 0, 'line_count' => $lineCount, 'file_content' => $fileContent, '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, ]; } /** * Wählt den passenden Parser für die Dateiendung. */ private function getParserForExtension(string $extension): PhpFileParser|PythonFileParser|JsFileParser { return match ($extension) { 'py' => $this->pythonParser, 'js' => $this->jsParser, default => $this->phpParser, }; } 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) ); } /** * Führt Quality-Analyse für alle Dateien des Scans aus. * * @return array{total_issues: int, avg_grade: string} */ private function runQualityAnalysis(string $scanId): array { $files = $this->repository->findByScanId($scanId); $totalIssues = 0; $gradeValues = []; $gradeMap = ['A' => 5, 'B' => 4, 'C' => 3, 'D' => 2, 'F' => 1]; foreach ($files as $file) { $qualityResult = $this->qualityChecker->analyze($file); $this->repository->saveQuality((int) $file['id'], $qualityResult); $totalIssues += $qualityResult['issues_count']; $gradeValues[] = $gradeMap[$qualityResult['quality_grade']] ?? 3; } $avgGradeValue = count($gradeValues) > 0 ? array_sum($gradeValues) / count($gradeValues) : 3; $avgGrade = match (true) { $avgGradeValue >= 4.5 => 'A', $avgGradeValue >= 3.5 => 'B', $avgGradeValue >= 2.5 => 'C', $avgGradeValue >= 1.5 => 'D', default => 'F', }; return [ 'total_issues' => $totalIssues, 'avg_grade' => $avgGrade, ]; } }