$this->getString('directory'), 'namespace' => $this->getString('namespace'), 'extension' => $this->getString('extension'), 'search' => $this->getString('search'), 'has_classes' => $this->getString('has_classes'), ], fn ($v) => $v !== ''); $this->view('docs.code', [ 'title' => 'Code-Analyse', 'stats' => $this->useCase->getStatistics(), 'files' => $this->useCase->execute($filters, 200, 0), 'filterOptions' => $this->useCase->getFilterOptions(), 'currentFilters' => $filters, ]); } public function show(string $id): void { $file = $this->useCase->findById((int) $id); if ($file === null) { $this->notFound('Datei nicht gefunden'); } $classes = json_decode($file['classes'] ?? '[]', true); $functions = json_decode($file['functions'] ?? '[]', true); $uses = json_decode($file['uses'] ?? '[]', true); $implements = json_decode($file['implements_interfaces'] ?? '[]', true); $traits = json_decode($file['traits_used'] ?? '[]', true); $constructorDeps = json_decode($file['constructor_deps'] ?? '[]', true); // Build FQCN for reverse lookup $dependents = []; if (!empty($classes)) { $namespace = $file['namespace'] ?? ''; $className = $classes[0]['name']; $fqcn = $namespace ? $namespace . '\\' . $className : $className; $dependents = $this->useCase->findDependents($fqcn); } // Backup history $backupHistory = $this->backupRepo->findByFilePath($file['file_path']); $this->view('docs.code-show', [ 'title' => $file['file_name'], 'file' => $file, 'classes' => $classes, 'functions' => $functions, 'uses' => $uses, 'extendsClass' => $file['extends_class'], 'implements' => $implements, 'traits' => $traits, 'constructorDeps' => $constructorDeps, 'dependents' => $dependents, 'backupHistory' => $backupHistory, ]); } public function scan(): void { $this->requireCsrf(); $result = $this->useCase->runScan('web'); $this->json([ 'success' => true, 'scan_id' => $result['scan_id'], 'files_scanned' => $result['files_scanned'], 'files_with_errors' => $result['files_with_errors'], 'total_classes' => $result['total_classes'], 'total_functions' => $result['total_functions'], 'duration_ms' => $result['duration_ms'], ]); } public function graph(string $id): void { $file = $this->useCase->findById((int) $id); if ($file === null) { $this->notFound('Datei nicht gefunden'); } $classes = json_decode($file['classes'] ?? '[]', true); $namespace = $file['namespace'] ?? ''; $className = !empty($classes) ? $classes[0]['name'] : $file['file_name']; $fqcn = $namespace ? $namespace . '\\' . $className : $className; $this->view('docs.code-graph', [ 'title' => 'Dependency Graph: ' . $className, 'file' => $file, 'fqcn' => $fqcn, ]); } public function graphData(string $id): void { $file = $this->useCase->findById((int) $id); if ($file === null) { $this->json(['error' => 'Not found'], 404); return; } $classes = json_decode($file['classes'] ?? '[]', true); $uses = json_decode($file['uses'] ?? '[]', true); $namespace = $file['namespace'] ?? ''; $className = !empty($classes) ? $classes[0]['name'] : $file['file_name']; $fqcn = $namespace ? $namespace . '\\' . $className : $className; $nodes = []; $links = []; // Central node $nodes[] = [ 'id' => $fqcn, 'label' => $className, 'type' => 'center', 'fileId' => $file['id'], ]; // Dependencies (outgoing) if ($file['extends_class']) { $shortName = $this->shortClassName($file['extends_class']); $nodes[] = ['id' => $file['extends_class'], 'label' => $shortName, 'type' => 'extends']; $links[] = ['source' => $fqcn, 'target' => $file['extends_class'], 'type' => 'extends']; } foreach (json_decode($file['implements_interfaces'] ?? '[]', true) as $iface) { $shortName = $this->shortClassName($iface); $nodes[] = ['id' => $iface, 'label' => $shortName, 'type' => 'implements']; $links[] = ['source' => $fqcn, 'target' => $iface, 'type' => 'implements']; } foreach (json_decode($file['constructor_deps'] ?? '[]', true) as $dep) { $shortName = $this->shortClassName($dep); $nodes[] = ['id' => $dep, 'label' => $shortName, 'type' => 'constructor']; $links[] = ['source' => $fqcn, 'target' => $dep, 'type' => 'constructor']; } // Dependents (incoming) - limit to 20 $dependents = $this->useCase->findDependents($fqcn); $count = 0; foreach ($dependents as $dep) { if ($count >= 20) { break; } $depFqcn = $dep['namespace'] ? $dep['namespace'] . '\\' . pathinfo($dep['file_name'], PATHINFO_FILENAME) : $dep['file_name']; $shortName = pathinfo($dep['file_name'], PATHINFO_FILENAME); if (!$this->nodeExists($nodes, $depFqcn)) { $nodes[] = ['id' => $depFqcn, 'label' => $shortName, 'type' => 'dependent', 'fileId' => $dep['id']]; } $links[] = ['source' => $depFqcn, 'target' => $fqcn, 'type' => $dep['dependency_type']]; $count++; } // Deduplicate nodes $uniqueNodes = []; foreach ($nodes as $node) { $uniqueNodes[$node['id']] = $node; } $this->json([ 'nodes' => array_values($uniqueNodes), 'links' => $links, 'center' => $fqcn, ]); } public function globalGraph(): void { $this->view('docs.code-graph-global', [ 'title' => 'Projekt Dependency Graph', ]); } public function globalGraphData(): void { $stats = $this->useCase->getGlobalGraphData(); $this->json($stats); } private function shortClassName(string $fqcn): string { $parts = explode('\\', $fqcn); return end($parts); } /** * @param array> $nodes */ private function nodeExists(array $nodes, string $id): bool { foreach ($nodes as $node) { if ($node['id'] === $id) { return true; } } return false; } }