amphp/process
Asynchronous process dispatcher for PHP (AMPHP) built for fibers and concurrency. Start and manage child processes cross-platform, stream stdout/stderr without blocking, set working directory and environment variables, powered by the Revolt event loop and Windows wrapper.
Start by installing via Composer:
composer require amphp/process
Ensure your project uses an AMP-based async framework (e.g., amphp/amp, Danack/Anvil, or AsyncPHP/HttpServer) since this package relies on the AMP event loop. The most basic usage spawns a process, waits for output, and exits:
use Amp\Process\Process;
use Amp\Promise;
require 'vendor/autoload.php';
async function main(): Promise {
$process = new Process(['php', '-r', 'echo "Hello from async!\n";']);
$process->start();
$output = '';
foreach ($process->getStdout() as $chunk) {
$output .= $chunk;
}
$exitCode = await $process->join();
echo "Exit code: $exitCode\nOutput: $output\n";
}
Amp\Loop::run(fn() => main());
Look first at the Process class documentation and the examples/ directory in the repo for quick, runnable demos.
stdout()/stderr() to consume data incrementally (e.g., tail logs or stream build output):
foreach ($process->getStderr() as $chunk) {
error_log("STDERR: $chunk");
}
write() and handle responses:
$process = new Process(['cat']);
$process->start();
$process->write("hello\n");
$process->write("world\n");
$process->closeStdin(); // Signal EOF
Amp\Promise + Amp\Timeout to kill hung processes:
$timedOut = false;
$process = new Process(['sleep', '10']);
$process->start();
try {
await $process->join(1000); // 1s timeout
} catch (Amp\TimeoutException $e) {
$timedOut = true;
$process->terminate();
}
$promises = [];
foreach ($files as $file) {
$process = new Process(['convert', $file, 'thumb_' . $file]);
$process->start();
$promises[] = $process->join();
}
await Amp\Promise\all($promises);
SIGINT/SIGTERM in CLI tools to gracefully kill child processes. Use the new getSignalName() helper to log human-readable signal names:
$process->onExit(fn(int $signal) => {
$signalName = \Amp\Process\getSignalName($signal);
Logger::info("Process terminated by signal {$signalName ?? $signal}");
});
getSignalName() to log meaningful signal names when handling process termination:
$signal = $process->getExitSignal();
$signalName = \Amp\Process\getSignalName($signal);
echo "Process exited with signal: {$signalName ?? 'unknown'}";
stdout/stderr, the process can deadlock due to full buffers (especially with interactive tools like ssh or python -i). Always yield or foreach the streams—even if you ignore the content.chdir() or pass absolute paths in $process->start([...], ['cwd' => '/path/to/dir']).write() returns a Promise<void> — you must await it if you rely on buffering guarantees. Failure to do so may drop input silently during high throughput.join() blocks until exit, but you can pass a timeout in milliseconds (e.g., await $process->join(5000)) to prevent indefinite hangs.terminate(), kill()) relies on POSIX signals — limited support on Windows. Prefer terminate() over kill() for cross-platform safety. Note that getSignalName() requires ext-pcntl and will return null on Windows or without the extension.proc_open(): Running synchronous calls in the same loop breaks async semantics. Use amphp/process exclusively for all process management when in an AMP context.ext-uv or ext-event or the native stream_select fallback — ensure your environment supports async I/O. Check with php -m | grep -E 'uv|event'.getSignalName() requires ext-pcntl: This helper function will return null if the signal number is not defined in the system or if ext-pcntl is unavailable. Handle this gracefully in production code:
$signalName = \Amp\Process\getSignalName($signal);
if ($signalName === null) {
// Fallback to raw signal number or custom logic
}
How can I help you explore Laravel packages today?