kinetis / persistence
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.
Requires
- php: ^8.4
- psr/log: ^3.0.2
- revolt/event-loop: ^1.0.9
Requires (Dev)
- infection/infection: ^0.35.0
- kinetis/framework: ^1.11.2
- phpstan/phpstan: ^2.2.8
- phpunit/phpunit: ^13.3.3
- vimeo/psalm: ^6.17
Suggests
- ext-mysqli: Needed by the native MySQL driver (Kinetis\Persistence\Driver\MysqliAsyncClient) — the auto driver's choice under FrankenPHP worker mode and RoadRunner.
- ext-pdo_mysql: Needed by the PDO MySQL driver (Kinetis\Persistence\Driver\PdoMysqlClient) — the auto driver's choice everywhere else, PHP-FPM included.
- ext-pdo_pgsql: Needed by the PDO Postgres driver (Kinetis\Persistence\Driver\PdoPgsqlClient) — the auto driver's choice everywhere else, PHP-FPM included.
- ext-pgsql: Needed by the native Postgres driver (Kinetis\Persistence\Driver\PgsqlAsyncClient) — the auto driver's choice under FrankenPHP worker mode and RoadRunner.
- ext-sockets: Needed alongside ext-pgsql by the native Postgres driver (Kinetis\Persistence\Driver\PgsqlAsyncClient), which ends a connection's transport with socket_shutdown() to abandon a statement without blocking the event loop.
Provides
None
Conflicts
None
Replaces
None
README
kinetis/persistence
Runtime-matched MySQL and Postgres clients with a transaction safety net
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 ownbeginTransaction()and held open across calls is tracked until the unit of work ends, where the host callsrollbackDangling()— from afinally, 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 throwsException\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.