strukt / process
Typed, resource-safe process execution and interactive shell prompts.
Requires
- php: ^8.1
- strukt/event: v1.1.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-25 15:45:38 UTC
README
Typed, resource-safe process execution for PHP 8.2 and later. Strukt Process
supports sequential command batches, line callbacks, captured output,
interactive prompt processes, supervised worker pools, and a supervise
command-line tool for long-running, daemonized supervised processes.
The package is a Strukt 3 rewrite of 'strukt2/process'. It keeps the original global helpers and process concepts while improving type declarations, resource cleanup, output draining, error capture, and documentation.
Installation
composer require strukt/process
The package depends on 'strukt/base', which supplies the helper registry.
Documentation
The full documentation lives in docs/: a topic guide, an architecture deep dive, a generated per-command reference, and a complete API reference. In particular:
- Quick start — from one command to a supervised daemon.
- Managed supervisor — daemonizing pools and the pid/state/log files that make them observable.
- Supervisor CLI — the
supervisebinary in detail. - Operations and troubleshooting — exit codes, failure modes, and debugging daemons.
Quick start
Run one command and inspect its output:
<?php $result = process('printf "hello\n"'); echo $result->outputs();
Stream complete output lines while a command runs:
$result = process( 'printf "first\nsecond\n"', static function (string $line): void { echo "received: {$line}"; }, );
process() waits for each command to finish, drains standard output and standard error concurrently, and returns a Strukt\ProcessResult.
Global helpers
The autoloaded src/helpers.php registers all process helpers with the Strukt helper registry.
| Helper | Parameters | Return type | Description |
|---|---|---|---|
| switchChannels | bool $switch = true | void | Swaps the logical output and error channels for subsequently created Process objects. |
| process | string|array<int|string, string> $commands, ?callable $callback = null | ProcessResult | Runs one command or a sequential command batch. |
| pprocess | string $command | PromptableProcessInterface | Starts an interactive process that accepts input through standard input. |
| pool | int $size, string|list $command, ?string $cwd = null, ?array $env = null | ProcessPool | Starts a pool of concurrent worker processes. |
| supervisor | int $size, string|list $command, int $restartCap = -1, float $backoff = 0.0, ?string $cwd = null, ?array $env = null | ProcessSupervisor | Starts a supervised pool that restarts dead workers under a restart policy. |
The helpers are available only when their names are not already defined, so they can coexist with applications that provide their own global functions.
Running commands
One command
$result = process('php -r "echo 2 + 2;"'); echo $result->outputs(); // 4 $process = $result->resource();
Multiple commands
Commands run in array order. Each command has its own process resource and captured output:
$result = process([ 'printf "compile\n"', 'printf "test\n"', ]); echo $result->outputs(0); // compile echo $result->outputs(1); // test $first = $result->resource(0); $second = $result->resource(1);
An empty command list returns an empty ProcessResult. Every command must be a non-empty string; invalid commands throw InvalidArgumentException.
Line callbacks
The callback receives complete logical standard-output lines, including their line endings. A final output fragment without a newline is delivered after the process closes its output stream.
$lines = []; process('printf "one\ntwo\n"', static function (string $line) use (&$lines): void { $lines[] = trim($line); }); // ['one', 'two']
Callbacks may also declare no parameters. Standard error is drained to avoid pipe back-pressure, but it is not sent to the callback.
ProcessResult
Strukt\ProcessResult extends ArrayIterator, so it can be iterated as a list of Strukt\Process objects.
resource
public function resource(int $idx = 0): Process
Returns the managed process at a zero-based command index. It throws OutOfBoundsException when the index is absent.
outputs
public function outputs(int $idx = 0): ?string
Returns captured logical standard output for a command. It returns null for an index that has no captured entry and an empty string when the command produced no output.
errors
public function errors(int $idx = 0): ?string
Returns captured logical standard error for a command. It follows the same null-versus-empty behavior as outputs.
Error output and exit codes
Both output channels are retained:
$result = process('sh -c "printf failure >&2; exit 7"'); echo $result->errors(); // failure echo $result->resource()->error(); // failure echo $result->resource()->close(); // 7, already reaped exit code
Process::run() throws RuntimeException when a command cannot be started. The process object uses InvalidArgumentException for invalid constructor handles or empty commands.
Switching channels
The default mapping is standard output to output() and standard error to error(). The mapping can be switched before starting a process:
switchChannels(true); $result = process('sh -c "printf output; printf error >&2"'); echo $result->outputs(); // error echo $result->errors(); // output switchChannels(false);
The setting affects new Process instances and remains enabled until changed. It does not change an already-created process.
Interactive processes
Use pprocess() when a command expects input:
$prompt = pprocess('read password; printf "%s\n" "$password"'); $prompt->write('secret'); $prompt->closeInput(); echo $prompt->read(); // secret
write() appends PHP_EOL to the supplied input and returns the number of bytes written. It throws RuntimeException when standard input is closed or the write fails. Close standard input when the child must receive EOF.
Interactive processes expose the common process methods as well:
$prompt->read(); $prompt->error(); $prompt->readline(); $prompt->isRunning(); $prompt->terminate(); $prompt->close();
Read output before calling close() if it has not already been captured. Calling closeInput(), closeOutput(), closeError(), closePipes(), or closeAll() more than once is safe.
Process pool
ProcessPool supervises a group of concurrently running workers: it spawns a fixed number of copies of one command, lets you drain their output without blocking, and tears them all down together. This fits the classic "spawn N workers, feed a queue, later stop the pool" loop where workers live for the whole batch rather than for one command.
$pool = new ProcessPool(3); $pool->start(PHP_BINARY . ' ' . escapeshellarg($workerScript)); while ($pool->running()) { $pool->drain(static function (int $index, string $line): void { printf("worker %d: %s", $index, $line); }); usleep(100000); } $pool->stop();
Start the pool with the pool() helper when you do not need the intermediate instance:
$pool = pool(3, [PHP_BINARY, $workerScript, '--prefix=demo']);
start
Spawns $size concurrent copies of the command, each with its own standard
three-pipe set so their output stays separate. A string command runs through
the shell; an array command runs the program directly. When the string is a
simple command (no pipes, redirection, quoting, or substitution), the pool
wraps it in exec so the shell replaces itself: the tracked pid is then the
real worker, and terminating the pool kills the worker rather than orphaning
a child of the shell. Standard input is closed up front because supervised
workers rarely read from the parent, and a left-open input pipe risks a
deadlock. Starting twice throws RuntimeException.
drain
Reads whatever each worker produced since the last poll without ever blocking on a worker. Complete standard-output lines go to the callback as (int $index, string $line); standard error is drained and retained on each worker (see Process::error()) but not dispatched. With no callback, drain() still records the output on the workers for later Process::output().
stop
Terminates every still-running worker and reaps its exit code, returning an array of codes keyed by spawn index.
Introspection
| Method | Return | Behavior |
|---|---|---|
| count | int | The configured pool size. |
| active | int | Workers still running. |
| running | bool | True while any worker runs. |
| processes | list<Process> | Workers in spawn order. |
| process($index) | Process | One worker by spawn index (throws OutOfBoundsException). |
The pool terminates its workers in __destruct() so an abandoned pool never leaves processes behind.
Process supervisor
ProcessSupervisor composes a ProcessPool with a restart policy: when a worker exits (crashes or finishes), supervise() replaces it with a fresh copy of the original command. The replacement keeps the same slot index, so callers that track per-slot state keep their numbering across restarts.
$supervisor = new ProcessSupervisor(3, restartCap: 5, backoff: 0.5); $supervisor->onExit(static function (int $index, int $exitCode): void { printf("worker %d exited with %d\n", $index, $exitCode); }); $supervisor->start(PHP_BINARY . ' ' . escapeshellarg($workerScript)); while (true) { $supervisor->supervise(static function (int $index, string $line): void { printf("worker %d: %s", $index, $line); }); usleep(100000); }
Start a supervisor with the supervisor() helper when you do not need the intermediate instance:
$supervisor = supervisor(3, [PHP_BINARY, $workerScript], restartCap: 5);
Policy
restartCapbounds how many times one slot may be restarted; once the cap is reached the slot is left dead. A cap of-1(default) never gives up.backoffthrottles restarts of the same slot: after a restart, at least$backoffseconds must pass before that slot restarts again, breaking tight crash loops.onExit()fires once whenever a worker is found dead -- whether or not it will be restarted -- so an owner can reclaim anything the dead worker left in flight (re-queue an un-finished job, log the crash, ...).
supervise
supervise() is the only thing a long-running owner needs to call in its loop. It drains worker output first (complete lines go to the optional callback as (int $index, string $line)), so the exit hook sees any final lines a worker wrote before dying, and then revives each dead slot under the policy.
Introspection
| Method | Return | Behavior |
|---|---|---|
| count | int | The configured pool size. |
| active | int | Workers still running. |
| running | bool | True while any worker runs. |
| processes | list<Process> | Workers in slot order. |
| process($index) | Process | One worker by slot index (throws OutOfBoundsException). |
| restarts($index) | int | How many times a slot has been restarted. |
| totalRestarts | int | All restarts across every slot. |
| stop | array<int, int> | Terminates every worker and returns exit codes keyed by slot. |
Managed supervisor
ProcessSupervisor supervises from inside the process that owns it. When the owner is a long-running job -- a queue consumer, a web worker, a scheduled task -- you usually want it to keep running as a background daemon and to be observable from other shells. Strukt\ManagedSupervisor adds that layer: it runs the same supervision loop as a detached daemon and persists everything worth seeing to a runtime directory.
use Strukt\ManagedSupervisor; $supervisor = new ManagedSupervisor('mail', size: 3, restartCap: 5); $supervisor->start(PHP_BINARY . ' ' . escapeshellarg($workerScript));
For every supervisor, the daemon writes three artifacts under the runtime
directory (default ~/.strukt/supervisor, overridable with the
STRUKT_SUPERVISOR_DIR environment variable or the $dir argument):
| Artifact | Purpose |
|---|---|
<name>/supervise.pid |
Pid of the daemon itself. |
<name>/state.json |
Snapshot of the pool written every supervision pass. |
<name>/output.log |
Combined worker output, one line per slot and supervisor events. |
The inspection surface is static on purpose -- status(), ps(), tail(),
kill() and stop() read the pid and state files and never join the daemon:
$state = ManagedSupervisor::status('mail'); // snapshot incl. workers $running = ManagedSupervisor::running('mail'); // true while the pid is alive $all = ManagedSupervisor::ps(); // every pool in the directory $tail = ManagedSupervisor::tail('mail', 50); // last 50 log lines ManagedSupervisor::kill('mail', 0); // SIGTERM one slot; respawns ManagedSupervisor::stop('mail'); // SIGTERM the daemon; reaps
The state snapshot includes the daemon pid, the command, the pool size, the per-slot worker pids, their running flag, exit code, and restart count, plus the total restart count and the log path.
Supervisor CLI
The package ships a supervise binary built on the Strukt console. It wraps
ManagedSupervisor with commands to start, list, inspect, kill, and stop
long-running supervised processes:
# daemonize a 2-worker pool that runs worker.php forever php vendor/bin/supervise start mail "php worker.php" --size=2 --dir=/var/run/sv # view every supervised pool php vendor/bin/supervise ps # show one pool's live state (workers, pids, restarts) php vendor/bin/supervise status mail # terminate one worker slot; the daemon restarts it in place php vendor/bin/supervise kill mail 0 # print the combined worker log php vendor/bin/supervise logs mail --lines=100 # stop the daemon (reaps workers, removes the pid file) php vendor/bin/supervise stop mail
Add --json to any command to render the result as JSON for scripting. Run
php vendor/bin/supervise list for the command reference, or generate
markdown documentation with php vendor/bin/supervise docs.
Process API
Process::run
public static function run( array $commands, ?callable $callback = null, ): ProcessResult
Runs commands sequentially and returns their resources and captured streams. The array values must be non-empty command strings.
Process::wait
public function wait(callable $callback): void
Drains the logical output and error pipes until the process exits. Complete output lines are passed to the callback. The implementation uses non-blocking pipes and stream_select() so a child that writes a large amount of standard error cannot block while the parent waits on standard output.
Process::output
public function output(): string
Returns all logical standard output captured by wait() or read().
Process::poll
public function poll(?callable $callback = null): bool
Drains whatever output is available right now without blocking, dispatching complete standard-output lines to the callback. Standard error is drained and retained only. A trailing fragment without a newline is delivered once the process exits. Returns true while the process is still running; used by ProcessPool::drain().
Process::switchChannels
public static function switchChannels(bool $switch = true): void
Changes the channel mapping used by subsequently constructed processes.
Process::__construct
public function __construct( mixed $process, mixed $stdin, mixed $stdout, mixed $stderr, )
Creates a process from the process and pipe resources returned by proc_open(). The four values must be live resources. PHP has no resource parameter type, so the implementation validates these arguments at runtime.
Interactive process API
Strukt\Pprocess extends Strukt\AbstractPromptableProcess.
Pprocess::run
public static function run(string $command): static
Starts a command with standard input, output, and error pipes and returns the interactive process instance.
Pprocess::__construct
public function __construct( mixed $process, mixed $stdin, mixed $stdout, mixed $stderr, )
Creates an interactive process around existing proc_open() resources.
Common process contract
Strukt\Contract\ProcessInterface defines the shared process operations:
| Method | Parameters | Return type | Behavior |
|---|---|---|---|
| getStatus | none | array | Returns the proc_get_status() array; throws when the process handle is closed. |
| read | none | string|false | Reads available standard output and retains it. |
| error | none | string|false | Reads available standard error and retains it. |
| readline | none | string|false | Reads one output line, including its line ending. |
| terminate | none | void | Terminates a running process; it is safe when already stopped. |
| close | none | int | Closes resources and returns the process exit code. |
| closeInput | none | bool | Closes standard input. |
| closeOutput | none | bool | Closes standard output. |
| closeError | none | bool | Closes standard error. |
| closePipes | none | void | Closes all three pipes. |
| closeAll | none | void | Terminates a running process and closes it. |
| isRunning | none | bool | Reports whether the process is still running. |
Strukt\Contract\PromptableProcessInterface extends this contract with:
public function write(string $input): int
The abstract implementations, AbstractProcess and AbstractPromptableProcess, provide the resource management used by Process and Pprocess. Their destructors are defensive and never allow cleanup exceptions to escape during shutdown.
For source compatibility with Strukt 2, the package also provides Strukt\Contract\AbstractProcess and Strukt\Contract\AbstractPromptableProcess as deprecated compatibility subclasses. New code should extend the root-namespace abstract classes.
Resource lifecycle
Process::run() closes standard input before waiting so commands receive EOF, drains both output channels, dispatches pending output, and reaps the child process. The captured strings remain available through ProcessResult and the returned Process objects after cleanup.
For manually managed Pprocess instances:
- Call write() as needed.
- Call closeInput() when no more input will be sent.
- Read output and error.
- Call close() or closeAll().
All process objects also attempt safe cleanup in __destruct().
Performance characteristics
- Output and error pipes are drained concurrently with non-blocking reads.
- The callback is invoked directly, avoiding one event wrapper per line.
- Command batches are sequential, preserving deterministic ordering.
- Captured output is appended in chunks rather than rebuilding the complete string for every line.
- Pipe and process cleanup is idempotent, reducing repeated resource checks in application code.
Development
Install development dependencies and run the Pest suite:
composer update
composer test
Validate the package metadata:
composer validate --no-check-publish
The test suite covers command batches, line callbacks, no-argument callbacks, standard-error capture, channel switching, interactive input, worker pools, cleanup, and helper registration.
License
Strukt Process is released under the MIT license.