pdo = $pdo; } /** * @param array $filters * @return array> */ public function findAll(array $filters = [], int $limit = 100, int $offset = 0): array { $sql = ' SELECT ca.*, cq.dependency_score, cq.hygiene_score, cq.hygiene_status, cq.factor_scores, cq.hardcoded_count, cq.issues_count, cq.warnings_count, cq.issues_json FROM code_analysis ca LEFT JOIN code_quality cq ON ca.id = cq.analysis_id WHERE 1=1 '; $params = []; if (!empty($filters['scan_id'])) { $sql .= ' AND ca.scan_id = :scan_id'; $params['scan_id'] = $filters['scan_id']; } if (!empty($filters['directory'])) { $sql .= ' AND ca.directory = :directory'; $params['directory'] = $filters['directory']; } if (!empty($filters['namespace'])) { $sql .= ' AND ca.namespace = :namespace'; $params['namespace'] = $filters['namespace']; } if (!empty($filters['extension'])) { $sql .= ' AND ca.extension = :extension'; $params['extension'] = $filters['extension']; } if (!empty($filters['search'])) { $sql .= ' AND (ca.file_name LIKE :search OR ca.namespace LIKE :search2)'; $params['search'] = '%' . $filters['search'] . '%'; $params['search2'] = '%' . $filters['search'] . '%'; } if (isset($filters['has_classes']) && $filters['has_classes'] !== '') { if ($filters['has_classes'] === '1') { $sql .= " AND ca.classes IS NOT NULL AND ca.classes != '[]'"; } else { $sql .= " AND (ca.classes IS NULL OR ca.classes = '[]')"; } } if (isset($filters['has_error']) && $filters['has_error'] === '1') { $sql .= ' AND ca.parse_error IS NOT NULL'; } if (!empty($filters['quality_grade'])) { $sql .= ' AND cq.quality_grade = :quality_grade'; $params['quality_grade'] = $filters['quality_grade']; } if (!empty($filters['has_issues'])) { $sql .= ' AND cq.issues_count > 0'; } $sql .= ' ORDER BY ca.directory, ca.file_name LIMIT :limit OFFSET :offset'; $stmt = $this->pdo->prepare($sql); foreach ($params as $key => $value) { $stmt->bindValue(':' . $key, $value); } $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(PDO::FETCH_ASSOC); } /** * @return array|null */ public function findById(int $id): ?array { $stmt = $this->pdo->prepare(' SELECT ca.*, cq.complexity_score, cq.loc_score, cq.dependency_score, cq.hardcoded_count, cq.issues_count, cq.warnings_count, cq.quality_grade, cq.issues_json FROM code_analysis ca LEFT JOIN code_quality cq ON ca.id = cq.analysis_id WHERE ca.id = :id '); $stmt->execute(['id' => $id]); $result = $stmt->fetch(PDO::FETCH_ASSOC); return $result ?: null; } /** * @param array> $items */ public function saveBatch(array $items, string $scanId): int { if (empty($items)) { return 0; } $this->pdo->beginTransaction(); try { $analysisStmt = $this->pdo->prepare(' INSERT INTO code_analysis (scan_id, file_path, file_name, extension, directory, file_size, line_count, file_content, modified_at, namespace, classes, functions, uses, extends_class, implements_interfaces, traits_used, constructor_deps, parse_error, triggered_by) VALUES (:scan_id, :file_path, :file_name, :extension, :directory, :file_size, :line_count, :file_content, :modified_at, :namespace, :classes, :functions, :uses, :extends_class, :implements_interfaces, :traits_used, :constructor_deps, :parse_error, :triggered_by) '); $depStmt = $this->pdo->prepare(' INSERT INTO code_dependencies (analysis_id, dependency_type, target_fqcn) VALUES (:analysis_id, :dep_type, :target_fqcn) '); $count = 0; foreach ($items as $item) { $analysisStmt->execute([ 'scan_id' => $scanId, 'file_path' => $item['file_path'], 'file_name' => $item['file_name'], 'extension' => $item['extension'], 'directory' => $item['directory'], 'file_size' => $item['file_size'], 'line_count' => $item['line_count'], 'file_content' => $item['file_content'] ?? '', 'modified_at' => $item['modified_at'], 'namespace' => $item['namespace'], 'classes' => json_encode($item['classes'] ?? [], JSON_UNESCAPED_UNICODE), 'functions' => json_encode($item['functions'] ?? [], JSON_UNESCAPED_UNICODE), 'uses' => json_encode($item['uses'] ?? [], JSON_UNESCAPED_UNICODE), 'extends_class' => $item['extends_class'], 'implements_interfaces' => json_encode($item['implements_interfaces'] ?? [], JSON_UNESCAPED_UNICODE), 'traits_used' => json_encode($item['traits_used'] ?? [], JSON_UNESCAPED_UNICODE), 'constructor_deps' => json_encode($item['constructor_deps'] ?? [], JSON_UNESCAPED_UNICODE), 'parse_error' => $item['parse_error'], 'triggered_by' => $item['triggered_by'] ?? 'web', ]); $analysisId = (int) $this->pdo->lastInsertId(); // Save dependencies to normalized table $this->saveDependencies($depStmt, $analysisId, $item); $count++; } $this->pdo->commit(); return $count; } catch (\Throwable $e) { $this->pdo->rollBack(); throw $e; } } /** * @param array $item */ private function saveDependencies(\PDOStatement $stmt, int $analysisId, array $item): void { // Use statements foreach ($item['uses'] ?? [] as $fqcn) { $stmt->execute(['analysis_id' => $analysisId, 'dep_type' => 'use', 'target_fqcn' => $fqcn]); } // Extends if (!empty($item['extends_class'])) { $stmt->execute(['analysis_id' => $analysisId, 'dep_type' => 'extends', 'target_fqcn' => $item['extends_class']]); } // Implements foreach ($item['implements_interfaces'] ?? [] as $fqcn) { $stmt->execute(['analysis_id' => $analysisId, 'dep_type' => 'implements', 'target_fqcn' => $fqcn]); } // Traits foreach ($item['traits_used'] ?? [] as $fqcn) { $stmt->execute(['analysis_id' => $analysisId, 'dep_type' => 'trait', 'target_fqcn' => $fqcn]); } // Constructor dependencies foreach ($item['constructor_deps'] ?? [] as $fqcn) { $stmt->execute(['analysis_id' => $analysisId, 'dep_type' => 'constructor', 'target_fqcn' => $fqcn]); } } public function deleteByNotScanId(string $currentScanId): int { $stmt = $this->pdo->prepare('DELETE FROM code_analysis WHERE scan_id != :scan_id'); $stmt->execute(['scan_id' => $currentScanId]); return $stmt->rowCount(); } public function getLatestScanId(): ?string { $stmt = $this->pdo->query('SELECT scan_id FROM code_analysis ORDER BY scanned_at DESC LIMIT 1'); $result = $stmt->fetchColumn(); return $result ?: null; } /** * @return array> */ public function findByScanId(string $scanId): array { $stmt = $this->pdo->prepare(' SELECT * FROM code_analysis WHERE scan_id = :scan_id ORDER BY file_path '); $stmt->execute(['scan_id' => $scanId]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } /** * @return array */ public function getStatistics(?string $scanId = null): array { $where = $scanId ? 'WHERE scan_id = :scan_id' : ''; $params = $scanId ? ['scan_id' => $scanId] : []; $stmt = $this->pdo->prepare(" SELECT COUNT(*) as total_files, SUM(line_count) as total_lines, SUM(file_size) as total_size, COUNT(CASE WHEN parse_error IS NOT NULL THEN 1 END) as files_with_errors, MAX(scanned_at) as last_scan FROM code_analysis {$where} "); $stmt->execute($params); $stats = $stmt->fetch(PDO::FETCH_ASSOC); $stmt = $this->pdo->prepare("SELECT classes, functions FROM code_analysis {$where}"); $stmt->execute($params); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $totalClasses = 0; $totalFunctions = 0; foreach ($rows as $row) { $classes = json_decode($row['classes'] ?? '[]', true); $functions = json_decode($row['functions'] ?? '[]', true); $totalClasses += count($classes); $totalFunctions += count($functions); } return [ 'total_files' => (int) ($stats['total_files'] ?? 0), 'total_lines' => (int) ($stats['total_lines'] ?? 0), 'total_size' => (int) ($stats['total_size'] ?? 0), 'total_classes' => $totalClasses, 'total_functions' => $totalFunctions, 'files_with_errors' => (int) ($stats['files_with_errors'] ?? 0), 'last_scan' => $stats['last_scan'], ]; } /** * @return array */ public function getDistinctDirectories(): array { $stmt = $this->pdo->query('SELECT DISTINCT directory FROM code_analysis ORDER BY directory'); return $stmt->fetchAll(PDO::FETCH_COLUMN); } /** * @return array */ public function getDistinctNamespaces(): array { $stmt = $this->pdo->query('SELECT DISTINCT namespace FROM code_analysis WHERE namespace IS NOT NULL ORDER BY namespace'); return $stmt->fetchAll(PDO::FETCH_COLUMN); } /** * @return array> */ public function getConfiguredDirectories(): array { $stmt = $this->pdo->query('SELECT * FROM code_scan_config WHERE enabled = 1 ORDER BY label'); return $stmt->fetchAll(PDO::FETCH_ASSOC); } }