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

Mysql Laravel Package

amphp/mysql

amphp/mysql is an asynchronous MySQL client for PHP built on Amp. It offers non-blocking queries, connection pooling, prepared statements, transactions, and streaming results for high-concurrency, event-driven applications.

View on GitHub
Deep Wiki
Context7

Getting Started

amphp/mysql is the async MySQL client from the Amp ecosystem — not an Eloquent/PDO drop-in. You reach for it when a single PHP process needs to hold many concurrent database conversations: fan-out queries, bulk ingest, websocket/long-running workers, or an Octane worker that shouldn't serialize its I/O.

Install and orient

composer require amphp/mysql
  • Requires PHP 8.1+ and amphp/amp v3 (Revolt event loop). It pulls in amphp/socket, amphp/sql, and amphp/file (used for TLS certificate loading).
  • Read in this order: src/MysqlConfig.php (how connections are described), src/MysqlConnectionPool.php (what you'll actually touch 95% of the time), src/MysqlConnection.php / MysqlTransaction.php, then the README examples.
  • There is no service provider and no facade. Laravel's container will not autowire anything for you — bind your own singleton if you want app-level access.

First use case: an Artisan command that queries concurrently

use Amp\Mysql\MysqlConfig;
use Amp\Mysql\MysqlConnectionPool;
use function Amp\async;
use function Amp\Future\await;

class WarmReports extends Command
{
    protected $signature = 'reports:warm {--account=*}';

    public function handle(): int
    {
        $config = MysqlConfig::fromString(
            'host=127.0.0.1 user=reports password=secret db=app'
        );
        $pool = new MysqlConnectionPool($config, maxConnections: 8);

        $futures = [];
        foreach ($this->option('account') as $id) {
            $futures[$id] = async(fn () => $pool->execute(
                'SELECT COUNT(*) AS c FROM orders WHERE account_id = ?', [$id]
            )->fetchRow());
        }

        foreach (await($futures) as $accountId => $row) {
            $this->line("{$accountId}: {$row['c']}");
        }

        $pool->close();
        return self::SUCCESS;
    }
}

If that feels like a lot of ceremony compared to DB::select(), that's the point: the ceremony is what buys you 8 queries in flight instead of 8 sequential round trips.

Implementation Patterns

Pool per process, not per query. MysqlConnectionPool is the unit of work. Construct it once (in a command, worker boot, or Octane worker lifecycle hook), reuse it, and close() on shutdown. Building a pool inside a loop defeats the purpose.

// AppServiceProvider::register() — only meaningful in long-running contexts
$this->app->singleton(MysqlConnectionPool::class, fn () => new MysqlConnectionPool(
    MysqlConfig::fromString(sprintf(
        'host=%s user=%s password=%s db=%s',
        config('database.connections.mysql.host'),
        config('database.connections.mysql.username'),
        config('database.connections.mysql.password'),
        config('database.connections.mysql.database'),
    )),
    maxConnections: 10,
));

Concurrency with a bound. Unbounded async() over a 50k-row import will queue 50k futures and exhaust the pool. Use Amp\Sync\LocalSemaphore or chunk the work:

$semaphore = new LocalSemaphore(8);
$futures = array_map(fn ($chunk) => async(function () use ($semaphore, $pool, $chunk) {
    $lock = $semaphore->acquire();
    try {
        return $pool->execute('INSERT INTO events (id, payload) VALUES '.$placeholders, $chunk)->getRowCount();
    } finally {
        $lock->release();
    }
}), array_chunk($rows, 500));

Transactions are connection-scoped. Get an explicit connection from the pool, then transact on it. Do not interleave $pool->execute() calls inside a transaction and expect them to be part of it.

$connection = $pool->getConnection();
$transaction = $connection->beginTransaction();
try {
    $transaction->execute('UPDATE accounts SET balance = balance - ? WHERE id = ?', [$amt, $from]);
    $transaction->execute('UPDATE accounts SET balance = balance + ? WHERE id = ?', [$amt, $to]);
    $transaction->commit();
} catch (Throwable $e) {
    $transaction->rollBack();
    throw $e;
} finally {
    $connection->close();
}

Stream, don't materialize. The result object is iterable; for large exports, foreach over it and write to a file handle rather than iterator_to_array(). This pairs naturally with amphp/file for the write side.

Sit it beside Eloquent, not instead of it. The pragmatic Laravel split: keep Eloquent/PDO for CRUD, validation, and anything touching model events; use amphp/mysql for the hot path in workers — analytics aggregation, third-party sync fan-out, log/metric ingestion — where you're issuing dozens of independent statements. Both can point at the same database; just budget max_connections for the sum of PHP-FPM workers, Octane workers, queue workers, and your pools.

Queue jobs. In a queue worker, wrap the job body in an Amp context and share the pool across jobs:

public function handle(MysqlConnectionPool $pool): void
{
    await([async(fn () => $pool->execute(...))]);
}

Keep an eye on worker restarts — pool->close() in a terminating/WorkerStopping listener so you don't leak sockets across deploys.

Error handling. Failures surface as Amp\Sql\SqlException (and Amp\Sql\ConnectionException / QueryError subtypes), not PDOException or Illuminate\Database\QueryException. If your app has global exception handling keyed on the Laravel types, add a mapping layer or you'll get unhelpful 500s.

Gotchas and Tips

Do not use this in a normal FPM request path. There's no event loop running; you'd be nesting Amp\Loop::run() inside a request, which is fragile and buys nothing over PDO. This package's value is entirely in processes that live longer than one request.

v3.1.1 is a safe patch. No API changes, no deprecations. Three things worth knowing:

  • MariaDB native UUID columns now have CI coverage and an integration test. MariaDB's UUID type is stored as 16 bytes; over the binary protocol you may receive raw bytes rather than the canonical xxxxxxxx-xxxx-... string you'd get over the text protocol. If you use UUID columns, normalize in PHP (bin2hex + hyphenation) or cast in SQL, and add a test on your actual server/version rather than assuming string semantics.
  • PHP 8.5 chr() deprecation was silenced in the integer encoding path. Behaviour is unchanged on 8.1–8.4; on 8.5 you just stop getting deprecation noise in logs. Nothing to do on your side.
  • amphp/[email protected] compatibility matters only if you use TLS with client certificates or a CA file — that's the code path reaching into amphp/file. If you pin amphp/file to 3.x elsewhere in your tree, Composer will resolve to the older amphp/mysql patch instead. Check composer why amphp/file after upgrading.

Pool size vs. max_connections. A pool of 10 in each of 20 queue workers is 200 connections. MySQL's default max_connections is 151. Set maxConnections deliberately and monitor Threads_connected; connection exhaustion here looks like random ConnectionExceptions under load, not an obvious error.

Prepared statements and the binary protocol. $pool->execute() uses prepared statements, so type handling differs from the text protocol you're used to with PDO. DECIMAL, BIGINT, BIT, and JSON columns can arrive as strings or byte strings. Don't assume int for an ID column — cast explicitly.

No migrations, no schema builder, no query builder. You're writing SQL strings. Identifier quoting is on you; parameters are ? placeholders only (no named binds). Wrap table/column names from user input in backticks and validate against an allowlist.

Timeouts are your responsibility. Long-running async queries won't be killed by max_execution_time in CLI contexts. Wrap with Amp\Timeout\timeout() and make sure a stuck query can't pin a pool slot forever.

Debugging. There's no query log. Add a thin wrapper around execute() that records the SQL, bindings, and elapsed time to Laravel's logger, or you'll be debugging blind. amphp/log + a Revolt loop watcher is useful for spotting a blocked loop — an accidental synchronous call (a file_get_contents, a sleep, an Eloquent query in the middle of an async block) stalls all your concurrency, and the symptom is uniform latency inflation, not an error.

Extension points. MysqlConfig covers host/port/user/password/db/charset/TLS (sslCa, sslCert, sslKey). For retry logic, metrics, or read/write splitting, wrap MysqlConnectionPool in your own class implementing Amp\Sql\ConnectionPool rather than forking the library — the pooled-connection API is stable and the interface is small.

Upgrade hygiene. Track the amphp/amp v3 and revolt/event-loop pins; most "it just stopped working" reports against this package trace back to a mismatched Amp major, not to MySQL. Run composer why-not amphp/amp ^3 after any dependency bump.

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.
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
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource