Database Connection
PDO-Verbindungsmanagement mit Connection-Pool, Read/Write-Splitting und Health-Checks.
Einführung
Datenbankverbindungen in PHP wirken simpel: new PDO(...) und los. In der Praxis braucht eine Produktionsanwendung aber Read/Write-Splitting über Replikas, Health-Checks mit Failover, Connection-Pooling und konsistente PDO-Optionen. Das alles manuell zu verdrahten bedeutet boilerplate-Code in jedem Projekt.
jardisadapter/dbconnection liefert eine Connection-Factory und einen Connection-Pool, der all das kapselt:
- Read/Write-Splitting — Queries automatisch an Reader-Replikas, Mutations an den Writer. Transparent für den Aufrufer
- Round-Robin und Random Load-Balancing — konfigurierbar pro Pool, mit automatischem Failover
- Health-Checks mit TTL-Caching —
SELECT 1mit konfigurierbarer Cache-Dauer. Gesunde Connections werden 30s gecacht, fehlerhafte sofort erneut geprüft - Reconnect-Support — automatischer Reconnect bei Health-Check-Failures
- External PDO Wrapping — bestehende PDO-Instanzen aus Legacy-Code oder Frameworks einbinden
- SQLite Auto-Optimierungen — WAL-Modus, Foreign Keys, Memory Temp Store automatisch bei jedem Connect
- MySQL, PostgreSQL, SQLite — drei Datenbanken, eine API
Installation
composer require jardisadapter/dbconnectionGitHub: jardisAdapter/dbConnection
Grundlegende Nutzung
Verbindungen erstellen
use JardisAdapter\DbConnection\Factory\ConnectionFactory;
$factory = new ConnectionFactory();
// MySQL
$mysql = $factory->mysql(
host: 'localhost',
user: 'app_user',
password: 'secret',
database: 'myapp',
port: 3306,
charset: 'utf8mb4',
);
// PostgreSQL
$postgres = $factory->postgres(
host: 'localhost',
user: 'app_user',
password: 'secret',
database: 'myapp',
port: 5432,
);
// SQLite
$sqlite = $factory->sqlite('/var/data/app.db');
$inMemory = $factory->sqlite(); // :memory:
// PDO verwenden
$pdo = $mysql->pdo();
$users = $pdo->query('SELECT * FROM users')->fetchAll();Bestehende PDO einbinden
// Legacy-PDO wrappen (Lifecycle bleibt beim Aufrufer)
$connection = $factory->fromPdo($existingPdo);
// Lifecycle an Jardis übergeben
$managed = $factory->fromPdo($existingPdo, manageLifecycle: true);
$managed->disconnect(); // PDO wird freigegebenConnection-Pool
Read/Write-Splitting
use JardisAdapter\DbConnection\ConnectionPool;
$pool = new ConnectionPool(
writer: $factory->mysql('primary.db', 'user', 'secret', 'myapp'),
readers: [
$factory->mysql('replica1.db', 'user', 'secret', 'myapp'),
$factory->mysql('replica2.db', 'user', 'secret', 'myapp'),
],
);
// Mutations → Writer
$pool->getWriter()->pdo()->exec("INSERT INTO orders (total) VALUES (99.99)");
// Queries → Reader (Round-Robin)
$orders = $pool->getReader()->pdo()->query("SELECT * FROM orders")->fetchAll();Pool ohne Replikas
Wenn keine Reader konfiguriert sind, wird der Writer für alle Operationen verwendet:
$pool = new ConnectionPool(writer: $factory->mysql(...));
$writer = $pool->getWriter();
$reader = $pool->getReader();
// $writer === $readerPool-Konfiguration
use JardisAdapter\DbConnection\Config\ConnectionPoolConfig;
$config = new ConnectionPoolConfig(
validateConnections: true, // Health-Checks aktivieren
healthCheckCacheTtl: 60, // Gesunde Connections 60s cachen
healthCheckNegativeCacheTtl: 0, // Fehlerhafte sofort erneut prüfen
loadBalancingStrategy: 'round-robin', // oder 'random'
);
$pool = new ConnectionPool(
writer: $writerConnection,
readers: $readerConnections,
config: $config,
);Failover
Wenn ein Reader den Health-Check nicht besteht, wird der nächste versucht. Erst wenn alle Reader ausgefallen sind, wird eine RuntimeException geworfen:
// Reader 1 ist down → automatisch Reader 2
$reader = $pool->getReader();
// Statistiken
$stats = $pool->getStats();
// ['reads' => 5, 'writes' => 2, 'failovers' => 1, 'readers' => 2]
$pool->resetStats();Health-Check-Mechanismus
isHealthy($connection):
1. Cache prüfen → gecachtes Ergebnis innerhalb TTL zurückgeben
2. SELECT 1 ausführen
→ Bei Fehler: reconnect() versuchen, SELECT 1 erneut
→ Wenn Reconnect fehlschlägt: false
3. Ergebnis cachen (positiv mit healthCheckCacheTtl, negativ mit healthCheckNegativeCacheTtl)Mit der Standard-Konfiguration (healthCheckNegativeCacheTtl = 0) wird eine fehlerhafte Connection bei jedem Aufruf erneut geprüft: kein Penalty-Fenster.
Transaktionen
$connection = $factory->mysql('localhost', 'user', 'secret', 'myapp');
$connection->beginTransaction();
try {
$connection->pdo()->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$connection->pdo()->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$connection->commit();
} catch (\Throwable $e) {
$connection->rollback();
throw $e;
}
$connection->inTransaction(); // falsePDO-Optionen
PDO-Optionen werden als Array übergeben und mit sicheren Defaults zusammengeführt:
// Standard-Optionen (immer aktiv):
// PDO::ATTR_ERRMODE → PDO::ERRMODE_EXCEPTION
// PDO::ATTR_DEFAULT_FETCH_MODE → PDO::FETCH_ASSOC
// PDO::ATTR_EMULATE_PREPARES → false
// Eigene Optionen ergänzen oder überschreiben:
$connection = $factory->mysql(
host: 'localhost',
user: 'app_user',
password: 'secret',
database: 'myapp',
options: [
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_TIMEOUT => 5,
],
);SQLite Auto-Optimierungen
SQLite-Connections erhalten automatisch bei jedem connect() und reconnect() diese PRAGMAs:
PRAGMA foreign_keys = ON
PRAGMA journal_mode = WAL
PRAGMA synchronous = NORMAL
PRAGMA temp_store = MEMORY
PRAGMA mmap_size = 30000000000Zusätzliche Methoden:
$sqlite = $factory->sqlite('/var/data/app.db');
$sqlite->getDatabasePath(); // '/var/data/app.db'
$sqlite->vacuum(); // VACUUM ausführenReconnect
Jeder Connection-Typ unterstützt reconnect():
| Typ | Verhalten |
|---|---|
| MySQL/PostgreSQL | PDO wird zerstört und neu aufgebaut |
| SQLite | PDO wird neu aufgebaut, PRAGMAs erneut angewandt |
| External | SELECT 1 Health-Check (kann PDO nicht neu erstellen) |
$connection->reconnect();
$connection->isConnected(); // trueArchitektur
Das Package folgt einem Factory + Config + Pool-Ansatz:
ConnectionFactory ← Factory
├── MySqlConfig → PdoConnection ← MySQL
├── PostgresConfig → PdoConnection ← PostgreSQL
├── SqliteConfig → SqLite ← SQLite (mit PRAGMAs)
└── ExternalConfig → External ← Bestehende PDO
ConnectionPool ← Orchestrator (Read/Write-Routing)
├── Writer (DbConnectionInterface)
├── Reader[] (DbConnectionInterface)
└── ConnectionPoolConfig ← Health-Check + Load-BalancingVerzeichnisstruktur
src/
├── ConnectionPool.php ← Pool mit Read/Write-Splitting
├── Config/
│ ├── ConnectionPoolConfig.php ← Pool-Konfiguration
│ ├── MySqlConfig.php ← MySQL DSN Builder
│ ├── PostgresConfig.php ← PostgreSQL DSN Builder
│ ├── SqliteConfig.php ← SQLite Config
│ └── ExternalConfig.php ← External PDO Config
├── Connection/
│ ├── PdoConnection.php ← Basis (MySQL/PostgreSQL)
│ ├── SqLite.php ← SQLite mit Auto-PRAGMAs
│ └── External.php ← External PDO Wrapper
└── Factory/
└── ConnectionFactory.php ← FactoryAPI-Referenz
ConnectionFactory
| Methode | Signatur | Beschreibung |
|---|---|---|
mysql | mysql(string $host, string $user, string $password, string $database, int $port = 3306, string $charset = 'utf8mb4', array $options = []): DbConnectionInterface | MySQL-Verbindung |
postgres | postgres(string $host, string $user, string $password, string $database, int $port = 5432, array $options = []): DbConnectionInterface | PostgreSQL-Verbindung |
sqlite | sqlite(string $path = ':memory:', array $options = []): DbConnectionInterface | SQLite-Verbindung |
fromPdo | fromPdo(PDO $pdo, bool $manageLifecycle = false): DbConnectionInterface | Bestehende PDO wrappen |
DbConnectionInterface
| Methode | Signatur | Beschreibung |
|---|---|---|
connect | connect(): void | Verbindung aufbauen |
pdo | pdo(): PDO | PDO-Instanz abrufen |
isConnected | isConnected(): bool | Verbindungsstatus |
disconnect | disconnect(): void | Verbindung trennen |
reconnect | reconnect(): void | Verbindung neu aufbauen |
beginTransaction | beginTransaction(): void | Transaktion starten |
commit | commit(): void | Transaktion committen |
rollback | rollback(): void | Transaktion zurückrollen |
inTransaction | inTransaction(): bool | In Transaktion? |
getDriverName | getDriverName(): string | mysql, pgsql, sqlite |
getDatabaseName | getDatabaseName(): string | Datenbankname |
getServerVersion | getServerVersion(): string | Server-Version |
ConnectionPool
| Methode | Signatur | Beschreibung |
|---|---|---|
getWriter | getWriter(): DbConnectionInterface | Writer-Verbindung |
getReader | getReader(): DbConnectionInterface | Reader (Round-Robin/Random) |
getReaders | getReaders(): array | Alle Reader |
getReaderCount | getReaderCount(): int | Anzahl Reader |
getStats | getStats(): array | Statistiken |
resetStats | resetStats(): void | Statistiken zurücksetzen |
Vollständiges Beispiel
Produktions-Setup mit Read/Write-Splitting, Health-Checks und Failover:
use JardisAdapter\DbConnection\ConnectionPool;
use JardisAdapter\DbConnection\Config\ConnectionPoolConfig;
use JardisAdapter\DbConnection\Factory\ConnectionFactory;
$factory = new ConnectionFactory();
// Writer + 2 Replikas
$pool = new ConnectionPool(
writer: $factory->mysql('primary.db.internal', 'app', $password, 'orders'),
readers: [
$factory->mysql('replica1.db.internal', 'app_ro', $password, 'orders'),
$factory->mysql('replica2.db.internal', 'app_ro', $password, 'orders'),
],
config: new ConnectionPoolConfig(
validateConnections: true,
healthCheckCacheTtl: 30,
loadBalancingStrategy: 'round-robin',
),
);
// Write → Primary
$pool->getWriter()->beginTransaction();
try {
$pdo = $pool->getWriter()->pdo();
$stmt = $pdo->prepare("INSERT INTO orders (customer_id, total) VALUES (?, ?)");
$stmt->execute([42, 299.99]);
$orderId = $pdo->lastInsertId();
$pool->getWriter()->commit();
} catch (\Throwable $e) {
$pool->getWriter()->rollback();
throw $e;
}
// Read → Replica (automatisches Failover bei Ausfall)
$orders = $pool->getReader()->pdo()
->query("SELECT * FROM orders WHERE customer_id = 42")
->fetchAll();
// Monitoring
$stats = $pool->getStats();
// ['reads' => 1, 'writes' => 1, 'failovers' => 0, 'readers' => 2]