Search by

kinetis / persistence

aln-1

Runtime-matched MySQL and Postgres clients (mysqli async, ext-pgsql async, PDO) built from a neutral connection definition, with a transaction safety net and injectable SQL instrumentation. Usable standalone; kinetis/database-bridge wires it into Kinetis.

Package info

github.com/kinetis-dev/persistence

pkg:composer/kinetis/persistence

Statistics

Installs: 131

Dependents: 8

Suggesters: 1

Stars: 0

Open Issues: 0

v1.4.1 2026-09-17 06:25 UTC

This package is auto-updated.

Last update: 2026-09-17 06:46:11 UTC


README

Kinetis

kinetis/persistence
Runtime-matched MySQL and Postgres clients with a transaction safety net

Packagist Version Packagist Downloads PHP Version License CI

Part of Kinetis, a non-blocking PHP framework for API-first applications, developed in the kinetis-dev/kinetis monorepo. Usable standalone: it depends on no Kinetis package.

MySQL and Postgres through runtime-matched drivers — native ext-mysqli/ext-pgsql async clients under a persistent worker, PDO under boot-and-die — all presenting this package's own Contract\SqlLink/Contract\SqlTransaction abstraction, so nothing above the driver needs to know which one it's talking to.

use Kinetis\Persistence\ConnectionDefinition;
use Kinetis\Persistence\Contract\SqlTransaction;
use Kinetis\Persistence\SqlConnectionFactory;
use Kinetis\Persistence\TransactionGuard;

$db = SqlConnectionFactory::create(new ConnectionDefinition(
    dialect: 'mysql',
    host: 'db.internal',
    database: 'shop',
    user: 'shop',
    password: $password,
));

// One guard per unit of work: a request, a job, a command.
$guard = new TransactionGuard($logger);

try {
    $guard->transaction($db, static function (SqlTransaction $tx): void {
        $tx->execute('UPDATE inventory SET stock = stock - 1 WHERE sku = ?', ['SKU-1']);
    });
} finally {
    $guard->rollbackDangling();
}

ConnectionDefinition holds the dialect (mysql or pgsql), the driver selection, host, port (the dialect's own by default), database, credentials, ConnectionOptions — charset, TLS, connect timeout, pool width — and how many connections to open at construction. SqlConnectionFactory::create() builds the client its driver selection names: native (mysqli or ext-pgsql async), pdo, or auto — native under FrankenPHP worker mode or RoadRunner, PDO everywhere else. SqlConnectionFactory::singleSession() builds a PDO client pinned to the first session it opens, which closes instead of reconnecting if that session is lost: the client for work that lives in the session, such as a session-scoped advisory lock.

What the host owns

  • A guard per unit of work. TransactionGuard::transaction() commits on success and rolls back on any throw. A transaction begun with the guard's own beginTransaction() and held open across calls is tracked until the unit of work ends, where the host calls rollbackDangling() — from a finally, so it runs whether the work returned or threw. It closes every tracked transaction still open, logs a warning for each, and rethrows the first cleanup failure once all have been attempted. A guard is never shared between units of work.
  • A client per connection for the process's lifetime. The async clients are connection pools: build each once at startup and reuse it across units of work. Under a persistent worker, warm the mysqli pool at startup (warmConnections); see the documentation for why.
  • Closing clients at shutdown. close() takes a client out of service and ends the connections it holds; every later call throws Exception\ConnectionException. Call it when the process stops using the client.

Instrumentation

Pass a Contract\SqlInstrumentation as either factory method's second argument to receive what every client and transaction reports: a query dispatched, sent to the server, and reaped; a transaction started, and ended as commit, rollback or unknown. A transaction's own BEGIN, COMMIT and ROLLBACK are not query moments — the started/ended pair is what reports them — while every statement run through the transaction reports all three. A client contains any failure its instrumentation throws, so instrumentation cannot change a query result, a transaction outcome or the release of a connection.

Every moment runs inline on the query's Fiber: an implementation must not suspend, must stay bounded, and must perform no blocking I/O — anything it exports goes to separately owned, bounded infrastructure. A client keeps its instrumentation for its whole lifetime, so the implementation holds no mutable request or unit-of-work state. queryDispatched() receives the complete SQL text; bound parameter values are never passed, but the text can carry literals, so never log or export it verbatim.

With Kinetis

composer require kinetis/database-bridge

kinetis/database-bridge builds the clients from DB_* configuration, binds the default connection, provides lazy request-scoped TransactionGuard cleanup, and reports through Kinetis telemetry.

Installation

composer require kinetis/persistence

Requires PHP 8.4+, plus the extension for the driver you use: ext-mysqli, ext-pgsql (with ext-sockets), ext-pdo_mysql or ext-pdo_pgsql. Full documentation: kinetis.dev/docs/persistence.html.

License

MIT — see LICENSE.