Install

composer require twilldb/twilldb

Requires PHP ≥ 8.1 with the FFI extension enabled — set ffi.enable=1 in php.ini, or pass -d ffi.enable=1. The embedded path also needs the native libengine: build it with cargo build -p twill-engine --release, or set TWILLDB_ENGINE_PATH to a built libengine.{so,dylib,dll}.

Quickstart (embedded)

<?php
require __DIR__ . '/vendor/autoload.php';

use Twill\Database;

// file://  embedded · s3://|r2://|gs://  storage-disaggregated
$db = Database::open('file://./local.db');
try {
    $db->exec('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)');
    $db->query('INSERT INTO notes (id, body) VALUES (?, ?)', [1, 'hello']);
    $rows = $db->query('SELECT id, body FROM notes');
    \print_r($rows);
} finally {
    $db->close();
}

Breakdown: the storage backend is chosen entirely by the URL scheme. Parameters bind positionally (?); values are never string-interpolated. Failures throw Twill\EngineError with a numeric ->status and a ->retryable flag.

API at a glance

CallEffect
Database::open($url)open a database (backend chosen by URL scheme)
$db->exec($sql)run DDL/DML, returns rows affected
$db->query($sql, $params = [])buffered rows; params bind positionally
$db->prepare($sql)reusable Statement (->all() / ->get() / ->run())
$db->transaction($fn)BEGIN/COMMIT, rollback on throw; commit blocks until durable
$db->branch($name)copy-on-write branch at the current LSN
$db->close()release the handle (idempotent)

Frameworks: Laravel, Symfony, CodeIgniter

There are two ways in, and the right choice depends on your runtime model.

1 — Embedded (persistent worker)

Open one Twill\Database in a singleton / service provider and reuse it across requests — the engine is a single writer per database. This shines on a long-lived worker (FrankenPHP, RoadRunner, Swoole), where the open + WAL-replay cost is paid once.

// A Laravel service provider (register the engine as a singleton).
$this->app->singleton(Twill\Database::class, fn () =>
    Twill\Database::open(env('TWILLDB_URL', 'file://./app.db'))
);

Classic php-fpm pays the cost per request

Under classic per-request php-fpm, each request opens and replays the WAL afresh. For that model, prefer server mode below (or move to a persistent worker).

2 — Server mode (PDO — no FFI)

Run engine-server (the same engine behind a Postgres-wire listener) and point your framework's Postgres connection at it. No FFI, no native library in the PHP process — frameworks already speak Postgres.

// Plain PDO against engine-server (cleartext — sslmode=disable).
$pdo = new PDO('pgsql:host=127.0.0.1;port=5433;dbname=main;sslmode=disable', 'postgres', '');
$pdo->exec('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)');
$stmt = $pdo->prepare('INSERT INTO notes (id, body) VALUES (?, ?)');
$stmt->execute([1, 'hello']);
  • Laravel: add a pgsql connection in config/database.php with host=127.0.0.1, port=5433, sslmode=disable.
  • CodeIgniter: use the Postgre database driver with the same DSN.
  • Symfony: set DATABASE_URL="postgresql://[email protected]:5433/main?sslmode=disable".

Next

Twill DB documentation · Licensed under BUSL-1.1. · Author