Skip to content

Repository

Generisches CRUD-Repository mit Raw-Data-Prinzip, Read/Write-Splitting und drei PK-Strategien.

Einführung

Die meisten Repository-Implementierungen koppeln sich an ein ORM: Entities rein, Entities raus, und dazwischen eine Schicht Magie. Wer nur ein sauberes CRUD-Interface über PDO braucht (ohne Hydration, ohne Unit-of-Work, ohne 50 Klassen) steht vor der Wahl zwischen zu viel Abstraktion oder zu wenig.

jardissupport/repository ist die bewusste Mitte. Ein schlankes, tabellenagnostisches Repository, das ausschließlich mit rohen Arrays arbeitet:

  • Raw Data Only — jede Methode akzeptiert und liefert array<string, mixed>. Keine Entities, kein ORM, keine Magie
  • Read/Write-Splitting — Queries gehen automatisch an die Reader-Connection, Mutations an den Writer. Transparent für den Aufrufer
  • Drei PK-StrategienAUTOINCREMENT, INTEGER (MAX+1 mit Retry) und NONE (vom Caller geliefert, z.B. UUID)
  • Konsistentes Exception-Handling — jeder Write-Fehler wird als PersistException geworfen. Kein rohes PDOException-Handling im Aufrufcode
  • DbQuery-Integration — flexible Queries über den fluenten SQL-Builder aus jardissupport/dbquery
  • Lazy Handler — jeder Handler wird erst beim ersten Aufruf instanziiert. Die Konstruktion eines Repositories kostet nahezu nichts

Installation

bash
composer require jardissupport/repository

GitHub: jardisSupport/repository

Abhängigkeiten:

PackageZweck
jardissupport/dbquerySQL-Builder für findByQuery()
jardissupport/contractsInterfaces und Exceptions

Grundlegende Nutzung

Mit einfacher PDO-Verbindung

php
use JardisSupport\Repository\Repository;

$pdo = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'pass');
$repository = new Repository($pdo);

// Insert
$id = $repository->insert('users', 'id', [
    'name'  => 'John Doe',
    'email' => 'john@example.com',
]);
// $id = 1 (autoincrement)

// Read
$user = $repository->findById('users', 'id', $id);
// ['id' => 1, 'name' => 'John Doe', 'email' => 'john@example.com']

// Update
$repository->update('users', 'id', $id, ['name' => 'Jane Doe']);
// true

// Delete
$repository->delete('users', 'id', $id);
// true

// Exists
$repository->exists('users', 'id', $id);
// false

Mit Connection-Pool (Read/Write-Splitting)

php
use JardisAdapter\DbConnection\ConnectionPool;
use JardisSupport\Repository\Adapter\PdoConnection;

// ConnectionPool erwartet DbConnectionInterface-Instanzen —
// rohe PDOs werden mit PdoConnection gewrappt.
$pool = new ConnectionPool(
    writer: new PdoConnection($writerPdo),
    readers: [new PdoConnection($reader1Pdo), new PdoConnection($reader2Pdo)],
);

$repository = new Repository($pool);

// findById → geht an Reader
$user = $repository->findById('users', 'id', 42);

// insert → geht an Writer
$id = $repository->insert('users', 'id', ['name' => 'John']);

Die Entscheidung Writer vs. Reader ist vollständig transparent: der Aufrufer muss nichts konfigurieren.

PK-Strategien

Drei Strategien für unterschiedliche Primärschlüssel-Modelle:

AUTOINCREMENT (Standard)

Für Tabellen mit Auto-Increment-Spalte. Der PK-Wert wird nicht in $values übergeben:

php
use JardisSupport\Contract\Repository\PrimaryKey\PkStrategy;

$id = $repository->insert('users', 'id', [
    'name'  => 'Alice',
    'email' => 'alice@example.com',
], PkStrategy::AUTOINCREMENT);
// $id = 1 (int, von PDO::lastInsertId())

INTEGER (MAX+1 mit Retry)

Für Tabellen ohne datenbankseite Auto-Increment-Sequenz. Der PK wird per SELECT MAX(pk) + 1 generiert:

php
$id = $repository->insert('legacy_table', 'id', [
    'name' => 'Bob',
], PkStrategy::INTEGER);
// $id = 1 (oder nächster freier Wert)

Bei einem Duplicate-Key-Fehler wird automatisch bis zu 3-mal mit einem frisch generierten PK wiederholt. Danach wird PersistException geworfen.

NONE (Caller liefert PK)

Für UUID-basierte oder extern generierte IDs. Der PK muss in $values enthalten sein:

php
$id = $repository->insert('orders', 'id', [
    'id'    => '550e8400-e29b-41d4-a716-446655440000',
    'total' => 299.99,
], PkStrategy::NONE);
// $id = '550e8400-e29b-41d4-a716-446655440000' (string)

Fehlt der PK in $values oder ist er weder int noch string, wird sofort eine PersistException geworfen.

Flexible Queries mit DbQuery

Für alles über findById() hinaus (Suche, Filter, Sortierung, Aggregation) akzeptiert findByQuery() einen vorbereiteten Query aus dem DbQuery-Builder:

php
use JardisSupport\DbQuery\DbQuery;

$query = (new DbQuery())
    ->select('*')
    ->from('users')
    ->where('status')->equals('active')
    ->where('age')->between(25, 35)
    ->orderBy('name', 'ASC')
    ->limit(10);

$users = $repository->findByQuery($query);
// Array von assoziativen Arrays

Weitere Query-Beispiele

php
// LIKE
$query = (new DbQuery())->select('*')->from('users')
    ->where('name')->like('Alice%');

// IN
$query = (new DbQuery())->select('*')->from('users')
    ->where('status')->in(['active', 'pending']);

// IS NULL
$query = (new DbQuery())->select('*')->from('users')
    ->where('email')->isNull();

// OR-Bedingung
$query = (new DbQuery())->select('*')->from('users')
    ->where('age')->lower(25)
    ->or('status')->equals('inactive');

// COUNT
$query = (new DbQuery())->select('COUNT(*) as total')->from('users')
    ->where('status')->equals('active');
$result = $repository->findByQuery($query);
// [['total' => 42]]

Batch-Delete

php
$repository->deleteAll('users', 'id', [1, 3, 5]);
// Löscht alle drei Zeilen in einem DELETE ... WHERE id IN (1, 3, 5)

// Leeres Array → No-Op
$repository->deleteAll('users', 'id', []);

Fehlerbehandlung

Alle Write-Operationen fangen PDOException und werfen stattdessen PersistException:

php
use JardisSupport\Contract\Repository\Exception\PersistException;

try {
    $repository->insert('users', 'id', ['name' => 'John']);
} catch (PersistException $e) {
    // Konsistenter Exception-Typ für alle Write-Fehler
    echo $e->getMessage();
}
MethodeFehlerfallException
insertLeere Values, DB-Fehler, fehlender PK bei NONE, falscher PK-Typ bei NONEPersistException
updateDB-FehlerPersistException
deleteDB-FehlerPersistException
deleteAllDB-FehlerPersistException
findByQueryQuery nicht preparedInvalidArgumentException

Update-Rückgabewerte

php
// Zeile gefunden und aktualisiert
$repository->update('users', 'id', 1, ['name' => 'Jane']);  // true

// Zeile nicht gefunden
$repository->update('users', 'id', 9999, ['name' => 'Ghost']);  // false

// Leere Values → No-Op, gibt true zurück
$repository->update('users', 'id', 1, []);  // true

PDO-Adapter

Für bestehende PDO-Instanzen, die als Contract-Interfaces verwendet werden sollen:

php
use JardisSupport\Repository\Adapter\PdoConnection;
use JardisSupport\Repository\Adapter\PdoConnectionPool;

// Einzelne Connection (DbConnectionInterface)
$connection = new PdoConnection($pdo);
$connection->beginTransaction();
$connection->commit();
$connection->rollback();
$connection->inTransaction();  // bool
$connection->getDriverName();  // 'mysql', 'pgsql', 'sqlite'
$connection->getDatabaseName(); // Datenbankname (multi-driver-aware)

// Connection Pool (ConnectionPoolInterface) — Reader = Writer
$pool = new PdoConnectionPool($pdo);

PdoConnection erkennt den Datenbank-Treiber automatisch und verwendet die richtige Methode für getDatabaseName():

  • MySQL: SELECT DATABASE()
  • PostgreSQL: current_database()
  • SQLite: PRAGMA database_list

Architektur

Das Repository folgt dem Closure-Orchestrator-Pattern mit lazy-initialisierten Handlern:

Repository                             ← Orchestrator (Facade)
├── ConnectionPool                     ← Writer/Reader-Routing
│   ├── QueryExecutor (Writer)         ← SQL-Ausführung (Mutations)
│   └── QueryExecutor (Reader)         ← SQL-Ausführung (Queries)
├── InsertHandler                      ← PK-Strategie-Dispatch
│   └── IntegerPkGenerator             ← MAX+1 mit Retry
├── UpdateHandler                      ← UPDATE by PK
├── DeleteHandler                      ← DELETE single row
├── DeleteAllHandler                   ← DELETE IN (ids)
├── FindByIdHandler                    ← SELECT WHERE pk = id
└── ExistsHandler                      ← SELECT 1 LIMIT 1

Verzeichnisstruktur

src/
├── Repository.php                  ← Orchestrator
├── Adapter/
│   ├── PdoConnection.php           ← PDO → DbConnectionInterface
│   └── PdoConnectionPool.php       ← PDO → ConnectionPoolInterface
└── Handler/
    ├── QueryExecutor.php           ← SQL-Ausführung + Dialekt-Erkennung
    ├── InsertHandler.php           ← Insert mit PK-Strategie
    ├── UpdateHandler.php           ← Update by PK
    ├── DeleteHandler.php           ← Delete single row
    ├── DeleteAllHandler.php        ← Delete batch
    ├── FindByIdHandler.php         ← Select by PK
    ├── ExistsHandler.php           ← Existence check
    └── IntegerPkGenerator.php      ← MAX+1 Generator

API-Referenz

Repository

MethodeSignaturBeschreibung
__construct__construct(ConnectionPoolInterface|PDO $connection)Pool oder einzelne PDO
insertinsert(string $table, string $pk, array $values, PkStrategy $strategy = AUTOINCREMENT): int|stringEinfügen mit PK-Strategie
updateupdate(string $table, string $pk, int|string $id, array $values): boolAktualisieren
deletedelete(string $table, string $pk, int|string $id): boolEinzelne Zeile löschen
deleteAlldeleteAll(string $table, string $pk, array $ids): voidBatch-Delete
findByIdfindById(string $table, string $pk, int|string $id): ?arrayZeile per PK laden
findByQueryfindByQuery(DbQueryBuilderInterface $query): arrayFlexible Query
existsexists(string $table, string $pk, int|string $id): boolExistenz prüfen

Vollständiges Beispiel

Ein typischer Use-Case mit UUID-PKs, Read/Write-Splitting und DbQuery:

php
use JardisSupport\Repository\Repository;
use JardisSupport\Contract\Repository\PrimaryKey\PkStrategy;
use JardisSupport\Data\Identity;
use JardisSupport\DbQuery\DbQuery;

$repository = new Repository($connectionPool);
$identity   = new Identity();

// Neuen Datensatz mit UUID erstellen
$orderId = $identity->generateUuid7();
$repository->insert('orders', 'id', [
    'id'          => $orderId,
    'customer_id' => $customerId,
    'total'       => 299.99,
    'status'      => 'pending',
    'created_at'  => date('Y-m-d H:i:s'),
], PkStrategy::NONE);

// Order-Lines als Batch einfügen
foreach ($items as $item) {
    $repository->insert('order_lines', 'id', [
        'id'         => $identity->generateUuid7(),
        'order_id'   => $orderId,
        'product_id' => $item['product_id'],
        'quantity'   => $item['quantity'],
        'price'      => $item['price'],
    ], PkStrategy::NONE);
}

// Aktive Bestellungen eines Kunden laden (→ Reader-Connection)
$query = (new DbQuery())
    ->select('*')
    ->from('orders')
    ->where('customer_id')->equals($customerId)
    ->where('status')->in(['pending', 'confirmed'])
    ->orderBy('created_at', 'DESC')
    ->limit(20);

$orders = $repository->findByQuery($query);

// Einzelne Bestellung aktualisieren (→ Writer-Connection)
$repository->update('orders', 'id', $orderId, [
    'status'     => 'confirmed',
    'updated_at' => date('Y-m-d H:i:s'),
]);

// Existenz prüfen ohne Daten zu laden
if ($repository->exists('orders', 'id', $orderId)) {
    // ...
}