eonx-com/easy-async
EasyAsync is a Laravel package that simplifies running asynchronous work, letting you dispatch jobs and process tasks in the background without blocking requests. It offers a clean API for queue-based execution so your app stays fast and responsive.
eonx-com/easy-async is a thin abstraction over running external processes (shell commands, artisan invocations, binaries) without blocking the calling PHP process. Treat it as plumbing, not a framework: the value is in not hand-rolling proc_open, output buffering, and exit-code checks every time you need to shell out.
Because the package is at 0 stars, 0 dependents, and "docs" maturity, do not adopt it blind. Budget 30–45 minutes of source reading before wiring it into anything that matters.
1. Install and confirm what you actually got
composer require eonx-com/easy-async
Then immediately check:
composer.json in the package — look for extra.laravel.providers / aliases. If there's no auto-discovery, you must register the service provider manually in bootstrap/providers.php (Laravel 11+) or config/app.php.src/ tree. With a low-maturity package, the source is the documentation. Look for the main factory/manager class, the process wrapper, and any Contract/Interface directory.tests/ if present — the test suite is the most reliable spec for the API surface.2. First read: the public entry point
Find the class you're meant to instantiate (commonly a manager, factory, or pool). Note whether it is bound in the container as a singleton — that determines whether state (like a pool of running processes) leaks between calls in a long-lived worker.
3. First use case
The canonical day-one use case is moving a slow blocking call off the request path. Example: a controller currently does exec('pdftk ...') and takes 4 seconds. Replace it with:
// Illustrative — verify class/method names against src/
$process = app(ProcessManager::class)->create('pdftk', [$input, 'output', $path]);
$process->run(); // async start
// ... respond to the user, or hand the handle to a queued job
If the package's abstraction does not save you at least the proc_open + stream-select loop, it is not earning its dependency. That's the bar to test against.
Best for operations where the caller doesn't need the result — cache warming, thumbnail generation, log rotation, webhook delivery to a slow endpoint. Start the process, register a completion callback (or simply don't wait), and let the request return.
The trade-off: if the PHP process exits before the child finishes, orphaned processes accumulate. Prefer this inside a queue job rather than a web request, so the job's lifetime bounds the child's.
The highest-leverage usage. If you have 200 files to transcode and each takes 2s, sequential is 400s. A pool with concurrency 4 gets you to ~100s. Typical shape:
$pool = $manager->pool(concurrency: 4);
foreach ($files as $file) {
$pool->add($manager->create('ffmpeg', ['-i', $file, $target($file)]));
}
$pool->wait(); // blocks, but processes run concurrently
This is where the package genuinely pays for itself — hand-writing a correct pool with output draining and exit-code collection is fiddly and easy to get subtly wrong (deadlocks from full pipe buffers are the classic failure).
An alternative to a pool that composes better with Laravel's queue:
// Job
public function handle(ProcessManager $manager): void
{
$result = $manager->create($this->command, $this->args)->runAndWait();
if (! $result->isSuccessful()) {
throw new RuntimeException($result->getErrorOutput());
}
}
Dispatch N jobs, let queue:work provide the concurrency, and you inherit retries, backoff, failed_jobs, Horizon metrics, and timeouts for free. For most Laravel apps this is the better default than in-request pooling — the pool only wins when N is large enough that per-job queue overhead dominates, or when you need results synchronously.
Capture stdout/stderr separately and log stderr on non-zero exit. Route child output through your existing logger rather than letting it hit the terminal — under a supervisor, unattended output either goes nowhere or floods logs. If the package exposes incremental callbacks (per-line or per-chunk), use them for heartbeats so a hung child is distinguishable from a slow one.
Don't let the package's API leak into controllers or jobs. Put a small adapter in app/Support/ that exposes domain methods (generateThumbnail(), exportCsv()) and takes the package as a constructor dependency. When the package stalls at 0 stars and you swap it for Symfony Process or Illuminate\Process, you change one file.
Verify before you commit. Zero dependents means no ecosystem has battle-tested this. Check the last commit date, whether composer.json pins php/laravel/framework constraints that match your stack, and whether CI is green. An unmaintained async-process wrapper is a liability, not an asset — the failure modes are process leaks and hangs, which are expensive in production.
Argument escaping is your problem, not the package's. Confirm whether the API takes an array of args (safe, no shell) or a single command string (shell, injection risk). Never interpolate user input into a command string. If only the string form exists, use escapeshellarg() yourself.
Pipe buffer deadlocks. If you start a process and don't read its output, a chatty child fills the OS pipe buffer (~64KB) and blocks forever. Any abstraction that offers a "start now, read later" API must drain output continuously. Test with a command that emits >100KB, and with the disableOutput() equivalent if available.
Timeouts are not optional. Every external process needs one. Confirm the default timeout and set it explicitly per call. A command with no timeout that hangs will occupy a queue worker until your supervisor gives up.
Zombie processes and orphaned children. If your PHP process dies mid-flight, does the child die too? Look for signal handling, proc_terminate, or a destructor. In long-lived workers (queue:work, Octane), a manager bound as a singleton can retain handles across jobs — check for a reset/flush method and call it, or resolve the manager as a fresh instance per job.
Exit code 0 is not "success" for every tool. Some binaries return 0 with warnings on stderr. Decide per-integration whether to treat non-empty stderr as failure, and assert on expected output artifacts rather than trusting the code alone.
Environment differs between CLI and FPM. PATH, locale, and the working directory will differ from what you get in a shell. Use absolute binary paths in config, and pass an explicit cwd if the tool writes relative files.
Testing. You want a fake/sync mode — look for an interface you can bind to a stub, or a config toggle that runs processes synchronously. Assert on the process's command and args rather than executing the real binary. If neither exists, wrap the manager in your own interface (Pattern 5) so tests never touch the real thing.
Extension points to inspect. Common candidates: a custom process factory, an output formatter/parser, an event dispatched before/after execution, and a config file (config/easy-async.php) for defaults like timeout and concurrency. If none of these exist, the abstraction is shallow and you may get more mileage from Illuminate\Process (Laravel 10+) or Symfony\Component\Process\ProcessPool directly, both of which are maintained and documented.
Decision heuristic. Use this package only if its pool implementation is correct and saves you real code. Otherwise, Illuminate\Process for one-off commands plus queue jobs for concurrency covers 90% of Laravel async-process needs with zero third-party risk.
How can I help you explore Laravel packages today?