amphp/process
amphp/process is an asynchronous process dispatcher for PHP 8.1+ built on the Revolt event loop. It runs child processes on all major platforms, including Windows, with non-blocking access to stdout and stderr, plus support for custom working directories and environment variables.
exec(), shell_exec(), and proc_open() calls hidden in jobs, Artisan commands, and service classes — each with its own escaping bugs, its own timeout story, and no consistent way to stream output. This package is the "buy" option: a maintained, MIT-licensed abstraction covering process start, non-blocking stdin/stdout/stderr, exit codes, working directory, environment, signals, cancellation, and Windows parity. The decision it informs is whether subprocess handling becomes a standardized internal layer or stays ad hoc.Process::join(), signal handling (getSignalName(), added in 2.1.0), and a documented history of fixing zombie processes under PHP-FPM and hung-process cases on Windows are the difference between "we run child processes" and "we can promise graceful shutdown, bounded runtimes, and no orphaned processes." Those are operational promises with product consequences.amphp/byte-stream alongside it. The defensible PM position is usually: adopt at the edges (a background worker, an internal CLI tool, a media pipeline) behind a narrow internal interface, rather than propagating Amp primitives through application code.Consider it when:
amphp/http-client, amphp/socket, amphp/mysql, or any Revolt-based service. Here it is the obvious, idiomatic choice and adds no new paradigm.Look elsewhere when:
Process facade (Symfony Process underneath) is simpler, blocking is fine, and it needs no new dependency or mental model.setTimeout() is a one-liner most teams already know.For executives — one paragraph. amphp/process is a small, MIT-licensed PHP library for running and supervising child processes without freezing the application while they run. It is infrastructure glue, not a product feature: it replaces hand-rolled exec()/proc_open() code with a maintained abstraction that works identically on Linux, macOS, and Windows, supports streaming output and cancellation, and has a multi-year release history with steady bug-fix maintenance. Cost is effectively zero (no license, no service), and the strategic value is that process-heavy features — media processing, document generation, deployment tooling — become cheaper to run and possible to offer to Windows customers. The main risk is ecosystem concentration, which we contain by wrapping it behind an internal interface and piloting it in one non-critical worker.
For engineering — the case. One subprocess layer instead of N bespoke ones: consistent argument escaping, consistent environment and working-directory handling, non-blocking stdout/stderr through Amp\ByteStream with real backpressure, exit codes via Process::join(), cancellation via Cancellation, signal support, and a track record of fixing exactly the bugs you would otherwise hit — zombie processes under PHP-FPM (2.0.2), hangs on Windows when a child crashes (1.1.9), premature join() resolution when a child traps termination signals (1.1.0). Stable 2.x targets PHP 8.1+ and Revolt v1.x with a clean fiber-based API. The cost is adopting fibers and Revolt in the modules that use it; keep that surface small.
For engineering — the counter-case we should acknowledge. Laravel already depends on Symfony Process, and most teams' subprocess needs are "run one command, wait, read the exit code." For that, adding an async runtime is over-engineering. Reach for this package when concurrency, streaming, or long-lived supervision is the actual requirement — and only where the team can debug fiber-based code.
Recommendation. Treat as a contained, opportunistic adoption: wrap it behind a single internal ProcessRunner-style interface, pilot it on one throughput-sensitive workload (a media or document pipeline) with clear before/after numbers on wall-clock time and worker count, and keep the blocking Laravel/Symfony path as the default for ordinary one-shot commands. Revisit only if the dependency-count concern becomes an approval blocker, in which case the wrapper makes swapping out a contained change rather than a rewrite.
How can I help you explore Laravel packages today?