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
The child process starts with a copy of the parent's environment. withEnvironmentVariable() and withEnvironmentVariables() add or override variables, withoutEnvironmentVariable() removes a variable even when it is inherited, and withClearedEnvironment() starts the child from an empty environment, so only variables set afterwards are passed to it. When PHP runs under the cli or phpdbg SAPI, a command that leaves the environment unchanged passes the parent's environment on as is. Otherwise the inherited variables are copied from getenv(), which does not list variables whose name contains a space, . or [. Under php-fpm and php-cgi, getenv() can also list the request's FastCGI parameters, such as the fastcgi_param values set by the web server and the HTTP_* request headers, so the child inherits those too; withoutEnvironmentVariable() and withClearedEnvironment() keep them from it.
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.