← Code-Übersicht

PostgresCodeDateiRepository.php

Pfad: src/Infrastructure/Persistence/PostgresCodeDateiRepository.php

Ext: php

Größe: 7417 Bytes

Geändert: 2026-07-16 10:49:14+02

<?php
declare(strict_types=1);
namespace Demo\Infrastructure\Persistence;

use Demo\Domain\Code\CodeDatei;
use Demo\Domain\Code\CodeDateiRepository;
use PDO;
use PDOStatement;
use Throwable;

final class PostgresCodeDateiRepository implements CodeDateiRepository
{
    /**
     * @var PdoConnectionFactory Verbindungsfabrik für die Datenbank.
     */
    private PdoConnectionFactory $factory;

    /** @var PDO|null Lazy-loaded PDO-Verbindung. */
    private ?PDO $pdo = null;

    /**
     * @param PdoConnectionFactory $factory Verbindungsfabrik.
     */
    public function __construct(PdoConnectionFactory $factory)
    {
        $this->factory = $factory;
    }

    /**
     * @return array<int, CodeDatei> Alle aktuell bekannten Dateien.
     */
    public function liste(): array
    {
        $sql = 'SELECT id, relativer_pfad, datei_name, datei_erweiterung, code, code_hash,'
            . ' datei_groesse, datei_mtime, created_at, updated_at FROM code_datei ORDER BY relativer_pfad';
        $stmt = $this->pdo()->prepare($sql);
        $stmt->execute();
        $ausgabe = [];
        foreach ($stmt === false ? [] : $stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $ausgabe[] = $this->hydrate($row);
        }
        return $ausgabe;
    }

    /**
     * @param int $id Primärschlüssel der gewünschten Datei.
     * @return CodeDatei|null Gefundene Entität oder null.
     */
    public function byId(int $id): ?CodeDatei
    {
        $sql = 'SELECT id, relativer_pfad, datei_name, datei_erweiterung, code, code_hash,'
            . ' datei_groesse, datei_mtime, created_at, updated_at FROM code_datei WHERE id = :id';
        $stmt = $this->pdo()->prepare($sql);
        $stmt->execute([':id' => $id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        return $row === false ? null : $this->hydrate((array) $row);
    }

    /**
     * Synchronisiert aktuellen Scanstand mit den gespeicherten Datensätzen.
     *
     * @param array<int,mixed> $dateien Eingelesene Dateien aus dem Scanner.
     * @return void
     */
    public function synchronisiere(array $dateien): void
    {
        $pdo = $this->pdo();
        $pdo->beginTransaction();
        try {
            $bestehende = $this->bestehendeZeilen();
            $einsetzen = $pdo->prepare(
                'INSERT INTO code_datei (relativer_pfad, datei_name, datei_erweiterung, code, code_hash,'
                . ' datei_groesse, datei_mtime)'
                . ' VALUES (:relativer_pfad, :datei_name, :datei_erweiterung, :code, :code_hash,'
                . ' :datei_groesse, :datei_mtime)'
            );
            $aktualisieren = $pdo->prepare(
                'UPDATE code_datei'
                . ' SET datei_name = :datei_name, datei_erweiterung = :datei_erweiterung,'
                . ' code = :code, code_hash = :code_hash, datei_groesse = :datei_groesse,'
                . ' datei_mtime = :datei_mtime WHERE id = :id'
            );
            $entfernen = $pdo->prepare('DELETE FROM code_datei WHERE relativer_pfad = :relativer_pfad');

            foreach ($dateien as $datei) {
                if (!$datei instanceof CodeDatei) {
                    continue;
                }
                $this->importiereDatei($datei, $bestehende, $einsetzen, $aktualisieren);
                unset($bestehende[$datei->relativerPfad]);
            }
            foreach (array_keys($bestehende) as $zuLoeschen) {
                $entfernen->execute([':relativer_pfad' => $zuLoeschen]);
            }
            $pdo->commit();
        } catch (Throwable $throwable) {
            if ($pdo->inTransaction()) {
                $pdo->rollBack();
            }
            throw $throwable;
        }
    }

    /**
     * Entscheidet Insert/Update anhand vorhandener Datei und Hash.
     *
     * @param CodeDatei      $datei       Zu verarbeitende Datei.
     * @param array<string,array{id:int,hash:string}> &$bestehende Bereits bekannte Dateien nach Pfad.
     * @param PDOStatement   $einsetzen   Prepared INSERT.
     * @param PDOStatement   $aktualisieren Prepared UPDATE.
     * @return void
     */
    private function importiereDatei(CodeDatei $datei, array &$bestehende, PDOStatement $einsetzen, PDOStatement $aktualisieren): void
    {
        $pfad = $datei->relativerPfad;
        if (!isset($bestehende[$pfad])) {
            $this->speichereNeu($einsetzen, $datei);
            return;
        }
        if ($bestehende[$pfad]['hash'] !== $datei->codeHash) {
            $aktualisieren->execute([
                ':id' => $bestehende[$pfad]['id'],
                ':datei_name' => $datei->dateiName,
                ':datei_erweiterung' => $datei->dateiErweiterung,
                ':code' => $datei->code,
                ':code_hash' => $datei->codeHash,
                ':datei_groesse' => $datei->dateiGroesse,
                ':datei_mtime' => $this->zeitstempelFromDatei($datei->geaendert),
            ]);
        }
    }

    /**
     * Fügt eine neue Datei persistiert ein.
     *
     * @param PDOStatement $einsetzen Prepared INSERT.
     * @param CodeDatei    $datei     Neue Datei.
     * @return void
     */
    private function speichereNeu(PDOStatement $einsetzen, CodeDatei $datei): void
    {
        $einsetzen->execute([
            ':relativer_pfad' => $datei->relativerPfad,
            ':datei_name' => $datei->dateiName,
            ':datei_erweiterung' => $datei->dateiErweiterung,
            ':code' => $datei->code,
            ':code_hash' => $datei->codeHash,
            ':datei_groesse' => $datei->dateiGroesse,
            ':datei_mtime' => $this->zeitstempelFromDatei($datei->geaendert),
        ]);
    }

    /**
     * Lädt den aktuellen Datenbestand nach Pfad mit id/hash.
     *
     * @return array<string,array{id:int,hash:string}> Vorhandene Datensätze.
     */
    private function bestehendeZeilen(): array
    {
        $bestehende = [];
        $stmt = $this->pdo()->prepare('SELECT id, relativer_pfad, code_hash FROM code_datei');
        $stmt->execute();
        foreach ($stmt === false ? [] : $stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $bestehende[(string) $row['relativer_pfad']] = ['id' => (int) $row['id'], 'hash' => (string) $row['code_hash']];
        }
        return $bestehende;
    }

    /**
     * Hydriert eine Datenzeile in ein Domain-Objekt.
     *
     * @param array<string,mixed> $row Datenbankzeile.
     * @return CodeDatei
     */
    private function hydrate(array $row): CodeDatei
    {
        return new CodeDatei(
            (int) $row['id'],
            (string) $row['relativer_pfad'],
            (string) $row['datei_name'],
            (string) $row['datei_erweiterung'],
            (string) $row['code'],
            (string) $row['code_hash'],
            (int) $row['datei_groesse'],
            (string) $row['datei_mtime'],
            (string) $row['created_at']
        );
    }

    /**
     * Wandelt Datumsstring in UTC-Format für MySQL/Postgres-kompatible Speicherung.
     *
     * @param string $iso ISO Datumszeichenkette.
     * @return string
     */
    private function zeitstempelFromDatei(string $iso): string
    {
        return gmdate('Y-m-d H:i:s', strtotime($iso) ?: time());
    }

    /**
     * Liefert die Lazy-PDO-Verbindung.
     *
     * @return PDO
     */
    private function pdo(): PDO
    {
        return $this->pdo ??= $this->factory->create();
    }
}

Frühere Versionen

Version Zeitpunkt Operation
5 2026-07-16 12:49:36.486735+02 UPDATE