Process
composer require php-standard-library/process
The Process component provides a typed, non-blocking API for spawning and managing child processes, built on top of PSL's IO system.
It serves as an alternative to proc_*, symfony/process, and amphp/process, with an API inspired by Rust's std::process module adapted to PSL conventions.
Usage
use Psl\IO;
use Psl\Process\Command;
// Run a command and collect its output
$output = Command::create('echo')->withArguments(['Hello', 'from', 'process'])->output();
if ($output->status->isSuccessful()) {
IO\write_line('Output: %s', trim($output->stdout));
}
Command Construction
Command is an immutable builder. All with*() methods return a new instance.
Command::create(string $program)-- executes a program directly, bypassing the shell. Prevents shell injection and is the recommended approach.Command::shell(string $command)-- interpreted by the system shell (/bin/sh -con Unix,cmd.exeon Windows). Allows pipes, globbing, and variable expansion.
Stdio Configuration
Stdio controls what happens with each standard I/O stream:
Stdio::piped()-- pipe between parent and child (default for stdout/stderr)Stdio::inherit()-- child inherits the parent's descriptorStdio::null()-- attach to/dev/null(default for stdin)Stdio::tty()-- connect directly to the terminal, useful for interactive programs (Unix only)Stdio::fromStreamHandle($handle)-- use an existing stream handle
Examples
Collect Output
use Psl\IO;
use Psl\Process\Command;
$output = Command::create('php')->withArguments(['-r', 'echo "hello world";'])->output();
IO\write_line('stdout: %s', $output->stdout); // "hello world"
IO\write_line('stderr: %s', $output->stderr); // ""
Check Exit Status
use Psl\IO;
use Psl\Process\Command;
$status = Command::create('php')->withArguments(['-r', 'exit(42);'])->status();
IO\write_line('Successful: %s', $status->isSuccessful() ? 'true' : 'false'); // false
IO\write_line('Exit code: %d', $status->getCode()); // 42
Write to Stdin
use Psl\IO;
use Psl\Process\Command;
use Psl\Process\Stdio;
$child = Command::create('cat')->withStdin(Stdio::piped())->spawn();
$child->getStdin()->writeAll("hello\n");
$child->getStdin()->close();
$output = $child->getStdout()->readAll();
$child->wait();
IO\write_line('Read from child: %s', trim($output));
Shell Commands
use Psl\IO;
use Psl\Process\Command;
$output = Command::shell('echo hello && echo world')->output();
IO\write_line('stdout: %s', $output->stdout); // "hello\nworld\n"
Cancellation
use Psl\Async;
use Psl\DateTime\Duration;
use Psl\IO;
use Psl\Process\Command;
try {
Command::create('sleep')
->withArgument('60')
->status(new Async\TimeoutCancellationToken(Duration::seconds(1)));
} catch (Async\Exception\CancelledException) {
IO\write_line('Process was killed after timeout (as expected)');
}
Signals
use Psl\IO;
use Psl\Process\Command;
use Psl\Process\Signal;
use Psl\Process\Stdio;
$child = Command::create('sleep')->withArgument('60')->withStdout(Stdio::null())->withStderr(Stdio::null())->spawn();
// Allow the child process to start before signaling
usleep(50_000);
$child->signal(Signal::Terminate);
$status = $child->wait();
IO\write_line('Has been signaled: %s', $status->hasBeenSignaled() ? 'true' : 'false');
$signal = $status->getTerminationSignal();
IO\write_line('Termination signal: %s', $signal !== null ? $signal->name : 'none');
Environment Variables
use Psl\IO;
use Psl\Process\Command;
$output = Command::create('php')
->withArguments(['-r', 'echo getenv("APP_ENV");'])
->withEnvironmentVariable('APP_ENV', 'production')
->output();
IO\write_line('APP_ENV: %s', $output->stdout); // "production"
Non-Blocking Poll
use Psl\IO;
use Psl\Process\Command;
use Psl\Process\Stdio;
$child = Command::create('sleep')->withArgument('0.1')->withStdout(Stdio::null())->withStderr(Stdio::null())->spawn();
$status = $child->tryWait(); // null (still running)
IO\write_line('Still running: %s', $status === null ? 'true' : 'false');
// ... do other work ...
$status = $child->wait(); // blocks until done
IO\write_line('Finished with code: %d', $status->getCode());
See src/Psl/Process/ for the full API.