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-Strategien —
AUTOINCREMENT,INTEGER(MAX+1 mit Retry) undNONE(vom Caller geliefert, z.B. UUID) - Konsistentes Exception-Handling — jeder Write-Fehler wird als
PersistExceptiongeworfen. Kein rohesPDOException-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
composer require jardissupport/repositoryGitHub: jardisSupport/repository
Abhängigkeiten:
| Package | Zweck |
|---|---|
jardissupport/dbquery | SQL-Builder für findByQuery() |
jardissupport/contracts | Interfaces und Exceptions |
Grundlegende Nutzung
Mit einfacher PDO-Verbindung
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);
// falseMit Connection-Pool (Read/Write-Splitting)
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:
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:
$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:
$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:
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 ArraysWeitere Query-Beispiele
// 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
$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:
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();
}| Methode | Fehlerfall | Exception |
|---|---|---|
insert | Leere Values, DB-Fehler, fehlender PK bei NONE, falscher PK-Typ bei NONE | PersistException |
update | DB-Fehler | PersistException |
delete | DB-Fehler | PersistException |
deleteAll | DB-Fehler | PersistException |
findByQuery | Query nicht prepared | InvalidArgumentException |
Update-Rückgabewerte
// 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, []); // truePDO-Adapter
Für bestehende PDO-Instanzen, die als Contract-Interfaces verwendet werden sollen:
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 1Verzeichnisstruktur
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 GeneratorAPI-Referenz
Repository
| Methode | Signatur | Beschreibung |
|---|---|---|
__construct | __construct(ConnectionPoolInterface|PDO $connection) | Pool oder einzelne PDO |
insert | insert(string $table, string $pk, array $values, PkStrategy $strategy = AUTOINCREMENT): int|string | Einfügen mit PK-Strategie |
update | update(string $table, string $pk, int|string $id, array $values): bool | Aktualisieren |
delete | delete(string $table, string $pk, int|string $id): bool | Einzelne Zeile löschen |
deleteAll | deleteAll(string $table, string $pk, array $ids): void | Batch-Delete |
findById | findById(string $table, string $pk, int|string $id): ?array | Zeile per PK laden |
findByQuery | findByQuery(DbQueryBuilderInterface $query): array | Flexible Query |
exists | exists(string $table, string $pk, int|string $id): bool | Existenz prüfen |
Vollständiges Beispiel
Ein typischer Use-Case mit UUID-PKs, Read/Write-Splitting und DbQuery:
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)) {
// ...
}