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.

Stdio Configuration

Stdio controls what happens with each standard I/O stream:

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.