For years, the PHP async ecosystem has felt like a fragmented puzzle.
We have amazing foundational libraries and powerful extensions, but building a full-stack async application often means dealing with the dreaded "Function Coloring" problem, managing clunky process wrappers for CPU-heavy tasks, or relying on pcntl_fork which can lead to unpredictable state corruption.
I wanted an async ecosystem that felt native, cohesive, and elegant, built entirely on standard modern PHP.
So, I built the Hibla Ecosystem for PHP 8.4+.
It is a complete, cross-platform suite of packages covering the Event Loop, Fibers, Promises, HTTP, Synchronization, Cancellation, and Multi-processing. Here is a look at what modern PHP async looks like when all the pieces fit perfectly together.
The End of "Function Coloring"
If you've written async code in other languages, you know the "What Color is Your Function?" problem. The moment you use await inside a function, that function must be marked async. This changes its return type to a Promise, forcing every function that calls it to also become async. The "color" infects your entire codebase.
Hibla solves this entirely.
In Hibla, async() and await() are plain PHP functions. await() is context-aware: it checks if it is running inside a Fiber. If it is, it suspends cooperatively. If it isn't (e.g., at the top level of your script), it simply holds the script there and drives the event loop until the promise resolves.
This means you can write normal functions that use await(), and the caller gets to decide whether to run them synchronously or concurrently:
use function Hibla\{async, await};
use Hibla\HttpClient\Http;
// A plain function. No special keywords, no changed return type.
function getUser(int $id): array {
$response = await(Http::get("https://api.example.com/users/$id"));
return $response->json();
}
// 1. Works synchronously at the top level and event loop runs underneath automatically!
$user = getUser(1);
// 2. Works concurrently when wrapped in async() and no changes to getUser() needed!
$promises = [
async(fn() => getUser(1)),
async(fn() => getUser(2))
];
[$user1, $user2] = await(Promise::all($promises));
The "color" lives at the call site, not inside your functions. You can sprinkle async logic into existing codebases without rewriting your entire application.
Fractal Concurrency (Fibers + Processes)
Fibers (introduced in PHP 8.1) are perfect for I/O (like HTTP requests or DB queries). But if you try to parse a 50MB CSV inside a Fiber, your entire event loop freezes. Fibers do not run in parallel; they run concurrently on a single thread.
To get true CPU parallelism, Hibla includes Hibla Parallel: a cross-platform, self-healing multi-processing engine.
Because the entire ecosystem is built on a unified Promise interface, the Event Loop doesn't care if a Promise is waiting for an I/O Fiber or an OS-level Process. You can mix them seamlessly:
use Hibla\Parallel\Parallel;
use function Hibla\{async, await};
// Boot a persistent worker pool
$pool = Parallel::pool(size: 4)->boot();
// Mix I/O and CPU work in the exact same loop!
$results = await(Promise::all([
// I/O Bound: Handled by a Fiber (Non-blocking)
async(fn() => fetch_api_data()),
// CPU Bound: Handled by a background OS Process (True Parallelism)
$pool->run(fn() => crunch_heavy_numbers())
]));
No pcntl_fork cloning nightmares. No deadlocks. And yes, it works natively on Windows (Hibla automatically swaps anonymous pipes for socket descriptors to guarantee true non-blocking I/O across all operating systems).
Structured Cancellation (No More Orphaned Tasks)
When a user clicks "Cancel", or a 30-second API timeout hits, what happens to your background tasks? In many systems, the promises are ignored, but the underlying HTTP requests and background processes keep burning resources until they finish.
Hibla Cancellation introduces .NET-style CancellationTokens. You pass a token into your workflow, and when you cancel the source, everything tracked by that token aborts immediately.
use Hibla\Cancellation\CancellationTokenSource;
use function Hibla\{async, await};
// Automatically cancel everything if it takes longer than 10 seconds
$cts = new CancellationTokenSource(timeoutSeconds: 10.0);
$workflow = async(function () use ($cts) {
try {
// By passing the token, Hibla tracks the promise automatically
$user = await(Http::get('/api/user/1'), $cts->token);
// If the 10s timeout hits while this parallel process is running,
// Hibla literally kills the OS worker process instantly!
$report = await(Parallel::task()->run(fn() => build_report()), $cts->token);
return $report;
} catch (\Hibla\Promise\Exceptions\CancelledException $e) {
echo "Workflow aborted and all resources freed cleanly!";
}
});
await($workflow);
Under the hood, in-flight cURL requests are aborted, stream watchers are removed, and background OS processes are terminated via full tree-kill. Zero resource leaks.
Async Race Conditions (Yes, they exist in PHP now)
Because PHP is single-threaded, developers often assume we don't have to worry about race conditions. In async PHP, this assumption is dangerous.
Every time a Fiber calls await(), it yields control to the event loop. Another Fiber can wake up and modify shared state while your first Fiber is asleep.
To solve this, Hibla Sync provides async-aware Mutex and Semaphore primitives. They never block the OS thread; they simply queue Fibers cooperatively.
use Hibla\Sync\Mutex;
use function Hibla\{async, await};
$mutex = new Mutex();
$cache = [];
async(function () use ($mutex, &$cache, $key) {
// Only one Fiber can enter this block at a time.
// Other Fibers are queued non-blocking.
await($mutex->withLock(function () use (&$cache, $key) {
if (!isset($cache[$key])) {
// Safe! No other Fiber can sneak in while we are awaiting this heavy DB call.
$cache[$key] = await(fetch_from_database($key));
}
}));
});
Zero-Boilerplate Event Loop
At the heart of all this is the Hibla Event Loop. It automatically uses ext-uv (libuv) for massive scale, but falls back to a pure-PHP stream_select implementation if extensions aren't available.
The best part? You never have to call $loop->run().
Hibla utilizes PHP's register_shutdown_function. You just write your code, and when the synchronous script finishes, the loop automatically takes over and drains all pending async tasks, timers, and background processes.
use Hibla\EventLoop\Loop;
// That's it. Just schedule the work.
Loop::addTimer(1.0, fn() => echo "Fired after 1 second!\n");
// The script ends, and the loop automatically runs.
The Vision
PHP 8.4 is an incredibly powerful language. With Fibers, we have the tools to build systems with massive concurrency and fault-tolerant background processing natively.
The Hibla Ecosystem is my attempt to take those raw tools and mold them into a cohesive, elegant, and developer-friendly experience.
You can explore the source code, read the extensive documentation, and try it out today:
- hiblaphp/async - Context-independent async/await
- hiblaphp/promise - Promises and structured concurrency combinators
- hiblaphp/event-loop - The dual-driver core engine
- hiblaphp/parallel - Self-healing multi-processing clusters
- hiblaphp/cancellation - Structured cancellation tokens
- hiblaphp/sync - Async-aware Mutex and Semaphores
If you find this exciting for the future of PHP, please consider dropping a ⭐️ on the GitHub repositories. It helps immensely with visibility and keeps open-source momentum going!
What are your thoughts on the current state of Async PHP? Let me know in the comments!
Top comments (0)