wyrihaximus/react-child-process-promise
Promise-based wrapper around ReactPHP's child process component. Spawn child processes and handle their stdout, stderr, exit codes, and streams through promises, making asynchronous command execution simpler in ReactPHP applications.
composer require wyrihaximus/react-child-process-promise
This pulls in react/child-process, react/event-loop, react/promise, and (transitively) symfony/process. The package itself is a thin promise shim — the real behavior lives in React\ChildProcess\Process, so read its docs alongside this one.
Where to look first: the README, then src/ (there is essentially one class). Skim vendor/react/child-process/src/Process.php because that is where escaping, exit codes, and stream handling actually happen.
Minimal usage — run a command and get its output as a promise:
<?php
require 'vendor/autoload.php';
use React\EventLoop\Factory;
use WyriHaximus\React\ChildProcess\Promise\Process;
$loop = Factory::create();
$process = new Process($loop, 'printf "hello"');
$process->run()->then(
function ($output) { echo "OUT: {$output}\n"; },
function (\Throwable $e) { echo "ERR: {$e->getMessage()}\n"; }
);
$loop->run();
(Constructor argument order and the exact run() signature vary by version — confirm against src/Process.php before copying this into production.)
First real use case: an Artisan command or long-lived queue worker that shells out to ffmpeg, git, backup scripts, or npm builds and wants to fan several out in parallel instead of blocking one at a time. This package is not a fit for HTTP request handlers — Laravel's Process facade already covers synchronous calls.
Fan-out and gather. The main value is running N processes concurrently and joining the results:
use function React\Promise\all;
$promises = [];
foreach ($files as $file) {
$cmd = 'convert ' . escapeshellarg($file) . ' -resize 50% ' . escapeshellarg($file . '.thumb.jpg');
$promises[$file] = (new Process($loop, $cmd))->run();
}
all($promises)->then(
function (array $outputs) { /* keyed by $file */ },
function (\Throwable $e) { /* first rejection wins */ }
);
Pipelines. Chain then() to feed one command's output into the next, or return another promise from a callback to flatten it.
Streaming large output. If the wrapper only exposes a collected-output promise, do not use it for multi-megabyte output — reach the underlying React\ChildProcess\Process, attach a stdout listener, and write to a file or stream to the client. Aggregating into a string is a memory landmine.
Timeouts. react/child-process has none. Wrap it:
$timer = $loop->addTimer(30, function () use ($process) {
$process->terminate(); // SIGTERM; escalate to SIGKILL in a follow-up timer
});
$process->run()->then(
fn ($out) => $loop->cancelTimer($timer),
fn ($e) => $loop->cancelTimer($timer)
);
Laravel integration. Bind the loop as a singleton in a service provider, and bind a ProcessFactory that injects the loop plus defaults (cwd, env, logger, timeout). Controllers stay synchronous; the async path lives in Artisan commands or custom queue workers you start with $loop->run().
Error handling. Always pass both handlers. Log the full command string, exit code, and stderr — the rejection message from a shim is often just an exit code, which is useless alone.
Retries/backoff. Catch the rejection, $loop->addTimer($delay, fn () => retry()), and cap attempts.
Concurrency caps. Chunk the input (array_chunk($files, 8)) and await each batch; each promise is a real OS process, not a coroutine.
$loop->run() is active. There is no wait(), and you cannot await unless you add a coroutine bridge like Recoil. A common first bug is “my promise never resolves” — the loop was never started.then(). A synchronous sleep(), DB query, or file_get_contents on a URL stalls the loop and every sibling process with it.escapeshellarg() every interpolated value, or you have a command-injection vulnerability waiting to happen — especially if any part comes from user input.cmd vs sh semantics, proc_open quirks, and terminate() behavior all diverge. Test if win is in your deployment matrix.terminate() sends SIGTERM to the immediate child; shell wrappers like sh -c "long; task" can leave grandchildren running. Prefix with exec where possible, and consider process groups.ext-pcntl. Signal-based termination on Unix generally needs it. Confirm your images have it.react/promise v2. If your app pulls v3, otherwise()/done() are gone. Verify compatibility in composer.lock.react/child-process — you'd gain streaming, timeouts, and stderr control, and lose a dependency that may not track ReactPHP releases.PATH or env vars differ. Pass cwd and env explicitly.Process to isolate whether the failure is the command or the async plumbing; use ps aux to check for leaked processes after tests.Process that adds logging, timeouts, and env defaults, rather than subclassing — it keeps you one swap away from replacing the shim with your own implementation.How can I help you explore Laravel packages today?