Skip to content

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-CachingSELECT 1 mit 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

bash
composer require jardisadapter/dbconnection

GitHub: jardisAdapter/dbConnection

Grundlegende Nutzung

Verbindungen erstellen

php
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

php
// 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 freigegeben

Connection-Pool

Read/Write-Splitting

php
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:

php
$pool = new ConnectionPool(writer: $factory->mysql(...));

$writer = $pool->getWriter();
$reader = $pool->getReader();
// $writer === $reader

Pool-Konfiguration

php
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:

php
// 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

php
$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();  // false

PDO-Optionen

PDO-Optionen werden als Array übergeben und mit sicheren Defaults zusammengeführt:

php
// 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:

sql
PRAGMA foreign_keys = ON
PRAGMA journal_mode = WAL
PRAGMA synchronous  = NORMAL
PRAGMA temp_store   = MEMORY
PRAGMA mmap_size    = 30000000000

Zusätzliche Methoden:

php
$sqlite = $factory->sqlite('/var/data/app.db');
$sqlite->getDatabasePath();  // '/var/data/app.db'
$sqlite->vacuum();           // VACUUM ausführen

Reconnect

Jeder Connection-Typ unterstützt reconnect():

TypVerhalten
MySQL/PostgreSQLPDO wird zerstört und neu aufgebaut
SQLitePDO wird neu aufgebaut, PRAGMAs erneut angewandt
ExternalSELECT 1 Health-Check (kann PDO nicht neu erstellen)
php
$connection->reconnect();
$connection->isConnected();  // true

Architektur

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-Balancing

Verzeichnisstruktur

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       ← Factory

API-Referenz

ConnectionFactory

MethodeSignaturBeschreibung
mysqlmysql(string $host, string $user, string $password, string $database, int $port = 3306, string $charset = 'utf8mb4', array $options = []): DbConnectionInterfaceMySQL-Verbindung
postgrespostgres(string $host, string $user, string $password, string $database, int $port = 5432, array $options = []): DbConnectionInterfacePostgreSQL-Verbindung
sqlitesqlite(string $path = ':memory:', array $options = []): DbConnectionInterfaceSQLite-Verbindung
fromPdofromPdo(PDO $pdo, bool $manageLifecycle = false): DbConnectionInterfaceBestehende PDO wrappen

DbConnectionInterface

MethodeSignaturBeschreibung
connectconnect(): voidVerbindung aufbauen
pdopdo(): PDOPDO-Instanz abrufen
isConnectedisConnected(): boolVerbindungsstatus
disconnectdisconnect(): voidVerbindung trennen
reconnectreconnect(): voidVerbindung neu aufbauen
beginTransactionbeginTransaction(): voidTransaktion starten
commitcommit(): voidTransaktion committen
rollbackrollback(): voidTransaktion zurückrollen
inTransactioninTransaction(): boolIn Transaktion?
getDriverNamegetDriverName(): stringmysql, pgsql, sqlite
getDatabaseNamegetDatabaseName(): stringDatenbankname
getServerVersiongetServerVersion(): stringServer-Version

ConnectionPool

MethodeSignaturBeschreibung
getWritergetWriter(): DbConnectionInterfaceWriter-Verbindung
getReadergetReader(): DbConnectionInterfaceReader (Round-Robin/Random)
getReadersgetReaders(): arrayAlle Reader
getReaderCountgetReaderCount(): intAnzahl Reader
getStatsgetStats(): arrayStatistiken
resetStatsresetStats(): voidStatistiken zurücksetzen

Vollständiges Beispiel

Produktions-Setup mit Read/Write-Splitting, Health-Checks und Failover:

php
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]