mix/object-pool
Object pooling for PHP and Laravel: reuse expensive-to-create objects instead of allocating new ones, cutting allocation and garbage-collection overhead in hot paths. Configurable pool sizing with framework-friendly integration.
The package is small enough to read end-to-end. Start with composer require mix/object-pool, then open src/ and read the pool class itself (ObjectPool.php / Pool.php depending on version) before touching your business code — there is no extensive docs, and the source is the spec. Pay attention to the constructor signature (max size + factory callable) and the get() / put() contract.
Requirements: PHP 7.2+ and the Swoole extension with coroutine support enabled. The pool is built on Swoole\Coroutine\Channel, so it only behaves correctly inside a coroutine context. Outside one, you'll get either a hard error or a silently blocking call — neither is what you want in a Laravel CLI/Web process.
Minimal first use — wrap an expensive client (PDO, Redis, HTTP client, anything you'd otherwise new per request):
use Mix\Pool\ObjectPool; // verify exact namespace in src/
$pool = new ObjectPool(10, function () {
return new \PDO('mysql:host=127.0.0.1;dbname=app', 'user', 'pass', [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
]);
});
Swoole\Coroutine\run(function () use ($pool) {
$db = $pool->get();
try {
$db->query('SELECT 1');
} finally {
$pool->put($db);
}
});
That smoke test — one coroutine, one get, one put, no exceptions — is your baseline. If it hangs, you're outside a coroutine or the factory threw.
Acquire / release with try/finally. This is the pattern you'll write every day. Never let an exception escape between get() and put():
$client = $pool->get();
try {
return $client->call($someCommand);
} finally {
$pool->put($client);
}
Wrap this in a small repository or "manager" class so callers never touch get()/put() directly. In Laravel terms, bind the manager to the container and inject it, rather than injecting the raw pool.
Lazy factory with reconnect logic baked in. The factory runs whenever the pool needs to grow. Put your connect logic there, plus a fast health check if the resource is a network connection:
$factory = function () {
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379, 1.0);
$redis->select(0);
return $redis;
};
If you have flaky connections, additionally probe on get() in your manager wrapper (cheap PING, SELECT 1) and drop/replace the resource if the probe fails — don't push that into the pool itself unless you're ready to subclass it.
Per-worker pools, sized against concurrency. Swoole forks workers; each worker gets its own pool instance. Size the pool to max_coroutines_per_worker (not total workers). A common starting point is 10–50 per worker, tuned by observing how often get() blocks. Prefill at worker start with a loop of get()/put() cycles so the first user request isn't paying connection costs.
Layered design with sibling packages. For real projects, prefer mix/database or mix/redis (which are themselves built on this package) over rolling your own pool. Reach for mix/object-pool directly when you have a bespoke resource: an internal gRPC/Thrift client, an SSH handle, an Imagick or wkhtmltopdf process wrapper, a heavy SDK object with a handshake.
Warm-up and drain lifecycle. Fill the pool on WorkerStart, and on WorkerStop/WorkerExit iterate the pool and explicitly close resources (PDO, sockets). __destruct ordering under Swoole shutdown is not something to rely on.
Timeouts as a first-class concern. Every get() should have a bounded wait. If the pool's get() accepts a timeout argument or the underlying Channel::pop() has one, use it — a bare get() on an exhausted pool will hang the coroutine indefinitely, and in a request context that's indistinguishable from a deadlock.
Coroutine\run will either throw or block the whole worker. Guard with Swoole\Coroutine::getCid() > 0 in your manager, or only instantiate pools inside coroutine-aware entry points.put() drains the pool permanently. The channel capacity equals the max size; each leaked resource is a slot gone until the worker recycles. Log get()/put() pairs in dev, or add a counter around your manager to surface imbalances before they become an outage.put() can block too. If you're using a bounded channel and try to return more objects than capacity (e.g., a finally running twice due to a retry bug), the put() will park. Treat "returned the same object twice" as a bug worth a guard flag.WorkerStart (parent process), children inherit handles that the server may already have dropped. Always build pools in WorkerStart, or ensure the factory reconnects lazily.release step, or don't pool it at all.rollBack() in the finally if a transaction is still open, and reset session state (SET NAMES, SET SESSION, temp tables) if you've touched it.get() timeouts usually surface as RuntimeException or a Swoole channel timeout. Catch them at the manager boundary and translate to something your app understands (503 / retryable error). Don't retry in a tight loop — that just starves the pool further.count($pool) (or $channel->length()) on each get/put in a staging environment. A pool that never grows past 2 under load usually means a slow factory, not a slow app. A pool pinned at max with high get wait times means the size is too small or a leak is shrinking it.get/put (or the create callback) to add health checks and stats rather than monkey-patching call sites.-1/infinite timeouts in production; pick something like 500 ms–2 s and alert on timeouts, because they're your earliest signal of pool starvation.How can I help you explore Laravel packages today?