Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Amphp Sqlite3 Laravel Package

fabpot/amphp-sqlite3

fabpot/amphp-sqlite3 provides asynchronous, non-blocking access to SQLite3 databases for Amp-based PHP applications, using promises and coroutines so queries can run without blocking the event loop.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a thin async SQLite client built on Amp. It is not a Laravel database driver — it does not plug into DB, Eloquent, or the query builder. Treat it as a standalone client you reach for inside code that already runs on an Amp/Revolt event loop (CLI commands, long-running workers, custom daemons), not inside a normal HTTP request handled by PHP-FPM.

  1. Install:

    composer require fabpot/amphp-sqlite3
    

    This pulls in amphp/amp (and, depending on the version you pin, amphp/sql / revolt/event-loop). Confirm which major version resolved — the API changed between Amp v2 and v3.

  2. Prerequisites: ext-sqlite3 (the SQLite3 class, not pdo_sqlite), PHP 8.x, and a writable database file plus its containing directory (SQLite needs the directory for journal/WAL files).

  3. Look first at the Fabpot\AmpSqlite3\Sqlite3Connection and Sqlite3ConnectionPool classes. Everything routes through amphp/sql interfaces (SqlConnection, SqlConnectionPool), so the method surface (execute, query, prepare, beginTransaction) will feel familiar if you've used amphp/mysql.

  4. First use case — an Artisan command that does real concurrent I/O:

    // app/Console/Commands/AsyncReport.php
    public function handle(): int
    {
        return Amp\Loop::run(function () {
            $pool = new Fabpot\AmpSqlite3\Sqlite3ConnectionPool(
                new Fabpot\AmpSqlite3\Sqlite3ConnectionConfig(database_path('app.sqlite')),
                maxConnections: 4,
            );
    
            $result = $pool->execute('SELECT id, email FROM users WHERE active = ?', [1]);
            foreach ($result as $row) {
                // handle row
            }
    
            return 0;
        });
    }
    

    The win over PDO here is that while one query streams, you can await other futures (HTTP calls, file writes) without blocking. If you only have one query and nothing else to do concurrently, PDO is simpler and faster.

Implementation Patterns

Bind a pool in a service provider. Resolve the pool once, keyed to a named SQLite file, and inject it into the services that need it. Constructing a connection per call defeats the point of the pool.

$this->app->singleton(Sqlite3ConnectionPool::class, fn () => new Sqlite3ConnectionPool(
    new Sqlite3ConnectionConfig(config('database.connections.sqlite.database')),
    maxConnections: (int) config('database.async_pool_size', 4),
));

Keep this out of the default DatabaseServiceProvider flow — it's an opt-in binding, not a replacement for db.

Repository wrapper. Hide the pool behind a repository so callers stay Amp-agnostic in their signatures and you have one place to swap in PDO later if the async bet doesn't pay off.

final class UserRepository
{
    public function __construct(private SqlConnectionPool $pool) {}

    public function activeEmails(): iterable
    {
        foreach ($this->pool->execute('SELECT email FROM users WHERE active = 1') as $row) {
            yield $row['email'];
        }
    }
}

Fan-out reads. The pool is most useful when you issue many independent reads concurrently. Wrap each in Amp\async() and Future\awaitAll() them. SQLite in WAL mode handles concurrent readers fine; concurrent writers are serialized.

Transactions. Use $pool->beginTransaction() and keep the transaction inside a single coroutine. Don't interleave unrelated queries into an open transaction — the pool may hand them to a different connection and you'll get surprising isolation.

Integrate with a custom worker. If you've written your own queue consumer on Amp (or are evaluating amphp/redis for jobs), the pool slots in naturally: pop a job, run its DB work, all without blocking the loop. Do not try to run this inside Laravel's built-in queue:work loop — it's synchronous and PDO-backed.

Migrations stay with PDO. Keep php artisan migrate on the default PDO connection and let the async client be read-heavy at runtime. There's no schema builder here.

Testing. Forget RefreshDatabase. Spin the loop manually in the test, point at a temp file, and tear it down:

$db = tempnam(sys_get_temp_dir(), 'test') . '.sqlite';
// create schema with PDO or SQLite3, then run the pool against $db

Gotchas and Tips

  • No Eloquent, no query builder. You get parameterized SQL and row iteration. Anything richer means writing your own mapper. This is the single biggest reason this package won't replace DB in day-to-day Laravel work.
  • Don't call it from a web request. Under PHP-FPM there's no persistent loop; you'd be paying Amp's startup cost for zero concurrency benefit. It only makes sense where an event loop already exists or you explicitly start one.
  • Blocking is contagious. A single sleep(), file_get_contents() on a slow URL, or synchronous PDO call inside the loop stalls all in-flight queries. Audit every call inside a coroutine. Prefer Amp's filesystem/HTTP clients alongside it.
  • SQLITE_BUSY is a configuration problem. Enable WAL (PRAGMA journal_mode = WAL), set a busy_timeout (PRAGMA busy_timeout = 5000), and cap maxConnections low (2–4) for write-heavy workloads. Multiple writer connections on one file will fight.
  • WAL is not the default. SQLite defaults to rollback journal, which serializes readers against a writer. If you don't turn on WAL, your "concurrent reads" won't be concurrent.
  • ext-sqlite3 ≠ pdo_sqlite. They bind parameters and return types differently (SQLite's type affinity is loose). Rows come back as associative arrays; cast explicitly rather than relying on coercion.
  • Version drift is real. This is a 7-star, zero-dependent package. Pin an exact version, read the changelog of the Amp major you're on, and be prepared for the Loop::run() → Revolt transition to break examples you find online. Budget time to read the source — it's small.
  • Transaction scope. The pool may route beginTransaction() and subsequent execute() calls to different connections unless your amphp/sql version tracks transaction affinity. Verify this against the version you pin before trusting multi-statement transactions.
  • File permissions and paths. database_path() is fine, but the directory must be writable for -wal and -shm files. In Docker, a read-only volume mount silently breaks WAL.
  • No extension points worth relying on. There's no hook for registering custom SQLite functions or aggregates through the connection; if you need sqliteCreateFunction, drop to the raw SQLite3 handle — which means leaving the async layer.
  • Fall-back plan. Because there are zero dependents and minimal maintenance signal, isolate the pool behind your own interface (as in the repository pattern above). If the package stalls, swapping to PDO or amphp/mysql should be a one-file change, not a refactor.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console