*/ public function all(): array { $stmt = $this->db->pdo()->query('SELECT * FROM repo_configs ORDER BY sort_order ASC, id ASC'); return $stmt->fetchAll(); } /** @return array|null */ public function find(int $id): ?array { $stmt = $this->db->pdo()->prepare('SELECT * FROM repo_configs WHERE id = :id'); $stmt->execute(['id' => $id]); $row = $stmt->fetch(); return $row === false ? null : $row; } /** @param array $data @return array */ public function create(array $data): array { [$owner, $repo, $color] = $this->validate($data, null); $now = $this->db->now(); $stmt = $this->db->pdo()->prepare( 'INSERT INTO repo_configs (owner, repo, color, sort_order, created_at, updated_at) VALUES (:owner, :repo, :color, :sort_order, :created_at, :updated_at)' ); $stmt->execute([ 'owner' => $owner, 'repo' => $repo, 'color' => $color, 'sort_order' => $this->nextSortOrder(), 'created_at' => $now, 'updated_at' => $now, ]); return $this->find((int) $this->db->pdo()->lastInsertId()); } /** @param array $data @return array */ public function update(int $id, array $data): array { if ($this->find($id) === null) { throw new NotFoundException('Projekt nicht gefunden: ' . $id); } [$owner, $repo, $color] = $this->validate($data, $id); $stmt = $this->db->pdo()->prepare( 'UPDATE repo_configs SET owner = :owner, repo = :repo, color = :color, updated_at = :updated_at WHERE id = :id' ); $stmt->execute([ 'id' => $id, 'owner' => $owner, 'repo' => $repo, 'color' => $color, 'updated_at' => $this->db->now(), ]); return $this->find($id); } public function delete(int $id): void { if ($this->find($id) === null) { throw new NotFoundException('Projekt nicht gefunden: ' . $id); } $stmt = $this->db->pdo()->prepare('DELETE FROM repo_configs WHERE id = :id'); $stmt->execute(['id' => $id]); } /** * Prueft Pflichtfelder + Farbformat vorab, statt eine Verletzung der UNIQUE-Constraint * (owner, repo) als PDOException auflaufen zu lassen (Konvention wie * AgentRepository::updateActivity() fuer das Fremdschluessel-Pendant bei Tickets). * * @param array $data * @param int|null $excludeId - beim Update die eigene Zeile von der Duplikatspruefung ausnehmen. * @return array{0: string, 1: string, 2: string} */ private function validate(array $data, ?int $excludeId): array { $owner = trim((string) ($data['owner'] ?? '')); $repo = trim((string) ($data['repo'] ?? '')); $color = $data['color'] ?? null; if ($owner === '' || $repo === '') { throw new InvalidArgumentException('Felder "owner" und "repo" sind erforderlich.'); } if (!is_string($color) || !preg_match('/^#?[0-9a-fA-F]{6}$/', $color)) { throw new InvalidArgumentException('Feld "color" muss ein 6-stelliger Hex-Farbwert sein (z. B. "#fbca04").'); } if ($this->existsWithOwnerRepo($owner, $repo, $excludeId)) { throw new InvalidArgumentException(sprintf('Projekt "%s/%s" ist bereits konfiguriert.', $owner, $repo)); } return [$owner, $repo, '#' . ltrim($color, '#')]; } private function existsWithOwnerRepo(string $owner, string $repo, ?int $excludeId): bool { $sql = 'SELECT COUNT(*) FROM repo_configs WHERE owner = :owner AND repo = :repo'; $params = ['owner' => $owner, 'repo' => $repo]; if ($excludeId !== null) { $sql .= ' AND id != :exclude_id'; $params['exclude_id'] = $excludeId; } $stmt = $this->db->pdo()->prepare($sql); $stmt->execute($params); return ((int) $stmt->fetchColumn()) > 0; } private function nextSortOrder(): int { $row = $this->db->pdo()->query('SELECT COALESCE(MAX(sort_order), -1) + 1 AS next FROM repo_configs')->fetch(); return (int) $row['next']; } }