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.
Architecture fit. amphp/process is not a drop-in replacement for Laravel's process layer; it is a primitive for a different execution model. Laravel's default runtime is synchronous and process-per-request, while this package assumes a live Revolt event loop and fiber-based concurrency. It therefore fits cleanly in only a few places:
Amp\async, Revolt\EventLoop::run()).Inside a standard FPM request or a default queue:work job, you'd be booting a mini-runtime inside a synchronous worker. That works, but it inverts the failure mode: any blocking call in that fiber (Eloquent/PDO, file_get_contents, sleep, a synchronous HTTP client) stalls all concurrent children in that process. Verdict: strong fit for a purpose-built async execution tier; poor fit as the app-wide process runner.
Integration feasibility. The API surface is small and stable: static Process::start($command, workingDirectory:, environment:), getStdin/getStdout/getStderr, join(?Cancellation), kill(), signal(), getPid(), getWorkingDirectory(), getEnvironment(), escapeArgument(). v2 replaced promise-returning APIs with direct returns, which reads much closer to synchronous code and lowers the porting cost. Concurrency is delegated to Amp\ByteStream\pipe() and Amp\async(). Realistic integration is a thin adapter — one interface, two implementations (Symfony Process for sync paths, Amp for async paths) — so call sites don't fork and most engineers never touch Amp.
Technical risk.
revolt/event-loop and amphp/byte-stream v2. Version alignment with anything else in the tree that uses Amp (Octane, WebSocket servers, PHPUnit plugins) is the most likely Composer-level blocker, and it surfaces as "we can't upgrade Laravel" six months later.noexec temp mounts.ext-pcntl; without it you inherit the documented zombie risk. Independently, a SIGKILLed PHP worker leaves children running regardless of library.getSignalName() is ext-pcntl-only and returns null otherwise, so signal-based control flow must degrade gracefully.Key questions.
join() takes a Cancellation; do we standardize on it, and what happens to grandchildren spawned through shell wrappers on cancellation?amphp/byte-stream v1 or an older Revolt?Stack fit. Position this as infrastructure for a dedicated async execution tier, not as the application's process runner. Illuminate\Process / Symfony Process remains the default everywhere. The package sits behind a domain interface — something like ProcessRunner with runSync() and runAsync() — which keeps the blast radius to one directory and makes the choice reversible per workload.
Migration path.
exec()/shell_exec()/proc_open()/Process::run() call sites through it. Zero behavior change; this alone usually surfaces duplicated, inconsistent subprocess code.Compatibility. Pin amphp/process ^2 (PHP 8.1+), amphp/byte-stream ^2, revolt/event-loop ^1. Do not attempt to mix v1 and v2 — the promise-to-direct-return change is structural, not cosmetic. Require ext-pcntl in *nix worker images. Make the Windows decision explicit: either support it and account for the wrapper binary in image builds and PHAR packaging, or keep Windows on the sync path permanently and document it.
Sequencing.
kill(), and logs exit code + duration + bytes.Maintenance. Small surface area — one primary class plus a few functions — MIT, semver, changelog-only release notes. Realistic annual cost is dependency bumps plus periodic re-verification of the Windows wrapper path. The library isn't the ongoing cost; owning two execution models in one codebase is. Enforce the boundary with a lint rule or review checklist ("no blocking calls in async paths") or it will erode.
Support. No commercial backing, no SLA, community issue tracker. Fine for a thin primitive; the risk is that a bug you hit becomes one you fix upstream or work around. Confirm in advance that forking or vendoring a patched copy is acceptable to whoever owns risk.
Scaling. This is the actual prize: async dispatch decouples "concurrent child processes" from "PHP worker count," letting a handful of PHP processes hold many in-flight ffmpeg/Chrome/git jobs instead of provisioning one Horizon worker per concurrent child. The ceiling is usually the child processes' own CPU/RAM, not PHP. Practical limits to model: file descriptors (3+ per child, more on Windows — watch ulimit -n), fiber and buffer memory, and total container memory. Counterweight: scaling PHP workers is already well understood on our platform; a bespoke async tier brings its own supervision, health checks, and deploy story, which is new operational surface.
Failure modes to design for.
join(); without a Cancellation policy and kill escalation, hung children hang forever.ext-pcntl.noexec temp dirs; PHAR packaging is the historically fragile path.Ramp-up. The API is an hour of reading; the event-loop/fiber model is the ramp. Expect a half-day onboarding doc plus one complete worked example (a real transcoding or headless-browser job with drain, timeout, kill, and metrics) to make it safe. Keep the adapter interface deliberately boring so the majority of engineers never write Amp code directly and a small owning group maintains the async tier. Before committing to a roadmap, run an explicit spike on the Octane/Revolt coexistence question — and be candid in specs that this buys concurrency for I/O-bound subprocess work only, and does nothing for CPU-bound PHP.
How can I help you explore Laravel packages today?