KiProtokollRepository.php

Code Hygiene Score: 92

Keine Issues gefunden.

Dependencies 3

Klassen 1

Funktionen 14

Verwendet von 3

Versionen 8

Code

<?php

declare(strict_types=1);

namespace Infrastructure\Persistence;

// @responsibility: Persistenz für KI-Protokoll (Claude-Konversationen)

use Domain\Repository\KiProtokollRepositoryInterface;

class KiProtokollRepository implements KiProtokollRepositoryInterface
{
    private \PDO $pdo;

    public function __construct(\PDO $pdo)
    {
        $this->pdo = $pdo;
    }

    public function findById(int $id): ?array
    {
        $stmt = $this->pdo->prepare('SELECT * FROM protokoll WHERE id = ?');
        $stmt->execute([$id]);
        $result = $stmt->fetch(\PDO::FETCH_ASSOC);

        return $result !== false ? $result : null;
    }

    public function findLatest(int $limit = 20): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT id, timestamp, client_name, request, status, duration_ms
             FROM protokoll ORDER BY id DESC LIMIT ?'
        );
        $stmt->bindValue(1, $limit, \PDO::PARAM_INT);
        $stmt->execute();

        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }

    public function findPaginated(
        ?string $search = null,
        ?string $status = null,
        ?string $model = null,
        int $limit = 50,
        int $offset = 0
    ): array {
        $sql = 'SELECT id, timestamp, client_name, model_name, status, tokens_total, duration_ms,
                       LEFT(request, 200) as request_preview
                FROM protokoll WHERE 1=1';
        $params = [];

        if ($search !== null && $search !== '') {
            $sql .= ' AND (request LIKE ? OR response LIKE ? OR client_name LIKE ?)';
            $params[] = '%' . $search . '%';
            $params[] = '%' . $search . '%';
            $params[] = '%' . $search . '%';
        }

        if ($status !== null && $status !== '') {
            $sql .= ' AND status = ?';
            $params[] = $status;
        }

        if ($model !== null && $model !== '') {
            $sql .= ' AND model_name = ?';
            $params[] = $model;
        }

        $sql .= ' ORDER BY timestamp DESC LIMIT ? OFFSET ?';

        $stmt = $this->pdo->prepare($sql);
        $paramIndex = 1;
        foreach ($params as $value) {
            $stmt->bindValue($paramIndex++, $value);
        }
        $stmt->bindValue($paramIndex++, $limit, \PDO::PARAM_INT);
        $stmt->bindValue($paramIndex, $offset, \PDO::PARAM_INT);
        $stmt->execute();

        return $stmt->fetchAll(\PDO::FETCH_ASSOC);
    }

    public function countFiltered(?string $search = null, ?string $status = null, ?string $model = null): int
    {
        $sql = 'SELECT COUNT(*) FROM protokoll WHERE 1=1';
        $params = [];

        if ($search !== null && $search !== '') {
            $sql .= ' AND (request LIKE ? OR response LIKE ? OR client_name LIKE ?)';
            $params[] = '%' . $search . '%';
            $params[] = '%' . $search . '%';
            $params[] = '%' . $search . '%';
        }

        if ($status !== null && $status !== '') {
            $sql .= ' AND status = ?';
            $params[] = $status;
        }

        if ($model !== null && $model !== '') {
            $sql .= ' AND model_name = ?';
            $params[] = $model;
        }

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);

        return (int) $stmt->fetchColumn();
    }

    public function getStatistics(): array
    {
        $stmt = $this->pdo->query(
            'SELECT
                COUNT(*) as total,
                SUM(CASE WHEN status = "completed" THEN 1 ELSE 0 END) as completed,
                SUM(CASE WHEN status = "error" THEN 1 ELSE 0 END) as errors,
                COALESCE(SUM(tokens_total), 0) as tokens_total,
                COALESCE(SUM(duration_ms), 0) as duration_total
             FROM protokoll'
        );

        return $stmt->fetch(\PDO::FETCH_ASSOC);
    }

    public function getDistinctModels(): array
    {
        $stmt = $this->pdo->query(
            'SELECT DISTINCT model_name FROM protokoll WHERE model_name IS NOT NULL ORDER BY model_name'
        );

        return $stmt->fetchAll(\PDO::FETCH_COLUMN);
    }

    public function findPreviousId(int $id): ?int
    {
        $stmt = $this->pdo->prepare('SELECT id FROM protokoll WHERE id < ? ORDER BY id DESC LIMIT 1');
        $stmt->execute([$id]);
        $result = $stmt->fetchColumn();

        return $result !== false ? (int) $result : null;
    }

    public function findNextId(int $id): ?int
    {
        $stmt = $this->pdo->prepare('SELECT id FROM protokoll WHERE id > ? ORDER BY id ASC LIMIT 1');
        $stmt->execute([$id]);
        $result = $stmt->fetchColumn();

        return $result !== false ? (int) $result : null;
    }

    public function insert(
        string $clientName,
        string $request,
        string $model,
        string $requestIp
    ): int {
        $stmt = $this->pdo->prepare(
            'INSERT INTO protokoll (request_ip, client_name, request, request_timestamp, model_name, status)
             VALUES (:ip, :client, :request, NOW(6), :model, \'pending\')'
        );

        $stmt->execute([
            'ip' => $requestIp,
            'client' => $clientName,
            'request' => $request,
            'model' => $model,
        ]);

        return (int) $this->pdo->lastInsertId();
    }

    public function complete(
        int $id,
        string $response,
        int $durationMs,
        ?int $tokensInput,
        ?int $tokensOutput
    ): void {
        // Guard: duration never negative
        $durationMs = max(0, $durationMs);

        // Calculate tokens_total in repository (contract)
        $tokensTotal = ($tokensInput ?? 0) + ($tokensOutput ?? 0);

        $stmt = $this->pdo->prepare(
            'UPDATE protokoll SET
                response = :response,
                response_timestamp = NOW(6),
                duration_ms = :duration,
                tokens_input = :ti,
                tokens_output = :to,
                tokens_total = :tt,
                status = \'completed\'
             WHERE id = :id'
        );

        $stmt->execute([
            'response' => $response,
            'duration' => $durationMs,
            'ti' => $tokensInput,
            'to' => $tokensOutput,
            'tt' => $tokensTotal > 0 ? $tokensTotal : null,
            'id' => $id,
        ]);
    }

    public function fail(int $id, string $errorMessage): void
    {
        $stmt = $this->pdo->prepare(
            'UPDATE protokoll SET
                response_timestamp = NOW(6),
                status = \'error\',
                error_message = :error
             WHERE id = :id'
        );

        $stmt->execute([
            'error' => $errorMessage,
            'id' => $id,
        ]);
    }

    public function updateRequest(int $id, string $fullRequest): void
    {
        $stmt = $this->pdo->prepare(
            'UPDATE protokoll SET request = :request WHERE id = :id'
        );

        $stmt->execute([
            'request' => $fullRequest,
            'id' => $id,
        ]);
    }

    public function cleanupStale(int $minutesOld = 10): int
    {
        $stmt = $this->pdo->prepare(
            'UPDATE protokoll SET
                status = \'error\',
                error_message = \'Timeout: No response within configured minutes\'
             WHERE status = \'pending\'
             AND request_timestamp < NOW() - INTERVAL :minutes MINUTE'
        );

        $stmt->execute(['minutes' => $minutesOld]);

        return $stmt->rowCount();
    }
}
← Übersicht Graph