Process: Running External Programs
Nette\Utils\Process lets you run
external programs from PHP: feed them input, read their output, and react to how they finished. It's a friendly wrapper around
PHP's proc_open() that reports errors by throwing exceptions instead of returning false.
Installation:
composer require nette/utils
All examples assume the following alias is defined:
use Nette\Utils\Process;
The Simplest Use
Do you want to run a program and read what it printed? That's all it takes:
$process = Process::runExecutable('git', ['log', '-1', '--format=%H']);
echo $process->getStdOutput();
The first argument is the program to run, the second is the list of its arguments: the same things you would type on the
command line, just split into an array. The getStdOutput() method waits for the program to finish and returns
everything it wrote to its standard output.
That's the whole idea: you start a process, and then you ask it questions: is it still running, what did it print, how did it end. The rest of this page goes through those questions one by one.
Starting a Process
There are two ways to start a process, and the difference is worth understanding.
static runExecutable (string
$executable, array $arguments=[], ?array $env=null, array $options=[], mixed $stdin='',
mixed $stdout=null, mixed $stderr=null, ?string $directory=null, ?float $timeout=60): Process
Runs one specific program with a list of arguments. The arguments are handed to the program directly, so you never have to escape spaces, quotes, or other special characters. And because no shell is involved, there's no risk of shell injection. This is the safe choice, especially when any part of the command comes from user input:
$file = $_GET['file']; // could be anything, even '; rm -rf /'
$process = Process::runExecutable('wc', ['-l', $file]); // perfectly safe
The program is looked up in the system PATH if you don't give a full path. For running a PHP script, the
PHP_BINARY constant comes in handy:
$process = Process::runExecutable(PHP_BINARY, ['-v']);
static runCommand (string $command,
?array $env=null, array $options=[], mixed $stdin='', mixed $stdout=null, mixed
$stderr=null, ?string $directory=null, ?float $timeout=60):
Process
Runs a command string through the system shell (/bin/sh on Linux and macOS, cmd.exe on
Windows). That gives you shell features: pipes |, redirects >, variable expansion, command chaining
with &&, and so on:
$process = Process::runCommand('git log --oneline | head -n 20');
But because the shell parses the whole string, never build a runCommand() string out of untrusted input,
which is a classic security hole. When in doubt, use runExecutable() instead.
With this many parameters, pass them as named arguments, e.g.
Process::runExecutable('git', ['pull'], timeout: 30). The $options array is forwarded to
proc_open() for advanced needs, such as bypass_shell on Windows.
The Process Runs in the Background
Once started, the process runs alongside your PHP script: runExecutable() and runCommand()
return immediately and don't wait for it to finish. You decide when (and whether) to wait:
$process = Process::runExecutable('npm', ['install']);
// ... do other work here while npm runs ...
$process->wait(); // now block until it's done
In practice you rarely call wait() yourself, because getStdOutput(), getExitCode(),
isSuccess(), and ensureSuccess() all wait for the process automatically before giving you an answer.
Call wait() explicitly when you want to pass it a callback.
isRunning(): bool
Returns true while the process is still running, false once it has finished or been terminated. Handy
for doing other work in the meantime:
while ($process->isRunning()) {
// do something else for a while
usleep(100_000); // 100 ms
}
How Did It End?
Every finished process has an exit code: by convention 0 means success and any other number means some
kind of failure (what exactly depends on the program).
getExitCode(): int
Returns the exit code, waiting for the process to finish first if needed:
$code = Process::runExecutable('git', ['pull'])->getExitCode(); // e.g. 0
isSuccess(): bool
A shortcut for “did the exit code equal 0?”:
$process = Process::runExecutable('git', ['pull']);
if (!$process->isSuccess()) {
echo 'git failed: ' . $process->getStdError();
}
ensureSuccess(): void
Often you just want the program to succeed and to fail loudly otherwise. ensureSuccess() waits for the process and
throws Nette\Utils\ProcessFailedException if the exit code isn't 0:
Process::runExecutable('git', ['pull'])->ensureSuccess();
// execution continues only if git succeeded
Reading the Output
A process has two separate output streams: standard output (the normal results) and standard error (where programs usually report problems and diagnostics). Nette Utils keeps the two apart and, by default, captures both into memory so you can read them whenever you want.
getStdOutput(): string
Waits for the process to finish and returns everything it wrote to standard output:
$process = Process::runExecutable('date');
echo $process->getStdOutput();
getStdError(): string
The same, but for standard error:
$process = Process::runExecutable('some-tool', ['--do-stuff']);
if (!$process->isSuccess()) {
throw new RuntimeException('The tool failed: ' . $process->getStdError());
}
If you redirect an output stream (to a file, a resource, or
false), there is nothing in memory to return and the matching getter throws
Nette\InvalidStateException.
consumeStdOutput(): string
Sometimes you want to see the output as it arrives, without waiting for the process to end, for example to show progress. Each call returns the chunk of standard output that has appeared since the previous call:
$process = Process::runExecutable('long-running-tool');
while ($process->isRunning()) {
echo $process->consumeStdOutput(); // prints whatever is new
usleep(100_000); // 100 ms
}
echo $process->consumeStdOutput(); // the final piece, produced just before it ended
The call inside the last iteration of the loop already returns whatever the process printed right before exiting, so the
extra call after the loop is just a safety net. There's consumeStdError() for standard error too.
Watching the Output Live
Instead of polling with consumeStdOutput(), you can hand wait() a callback. It will be invoked every
time new output appears, which is great for live logging or forwarding the output somewhere:
$process = Process::runExecutable('npm', ['install']);
$process->wait(function (string $stdOut, string $stdErr) {
echo $stdOut; // forward standard output
fwrite(STDERR, $stdErr); // and standard error
});
The callback gets two strings: the new standard-output data and the new standard-error data since the previous call (either may
be empty). When wait() returns, the process is finished and you can still call getExitCode(),
getStdOutput(), and the rest.
Sending Input
The $stdin parameter says what the process reads on its standard input. It accepts a few different things.
A string becomes the process's entire input:
$process = Process::runExecutable('wc', ['-c'], stdin: 'hello world');
echo $process->getStdOutput(); // 11
A readable resource (an open file, a stream) is copied to the input:
$file = fopen('data.csv', 'r');
$process = Process::runExecutable('sort', stdin: $file);
null keeps the input open so you can write to it gradually (see below).
The default is an empty string, which means the process gets an empty, immediately-closed input. That's the sensible default: it stops programs that read input from hanging forever waiting for something that never comes.
writeStdInput (string $string): void
When you start the process with stdin: null, the input stays open and you feed it piece by piece. Call
closeStdInput() when you're done. That tells the program no more input is coming (it sends an end-of-file):
$process = Process::runExecutable('some-repl', stdin: null);
$process->writeStdInput("first command\n");
$process->writeStdInput("second command\n");
$process->closeStdInput();
echo $process->getStdOutput();
A string or stream passed as $stdin is written all at once before the process really gets going. If
that input is large and the program produces a lot of output without reading its input first, both sides can get stuck
waiting for each other. In that (rare) case, use stdin: null and writeStdInput() to interleave writing
with reading.
Chaining Processes (Piping)
You can connect one process's standard output straight to another's standard input, exactly like a shell pipe |.
Just pass a Process as the $stdin:
$producer = Process::runExecutable('cat', ['big.log']);
$consumer = Process::runExecutable('grep', ['error'], stdin: $producer);
echo $consumer->getStdOutput();
You can chain as many processes as you like (a | b | c).
Piping processes together is not supported on Windows (it throws Nette\NotSupportedException).
On Windows, capture the first process's output with getStdOutput() and pass it to the next one as a string.
Redirecting the Output Elsewhere
By default, standard output and standard error are captured into memory. The $stdout and $stderr
parameters let you send them somewhere else instead.
A filename sends the output to that file:
Process::runExecutable('mysqldump', ['mydb'], stdout: 'backup.sql')
->ensureSuccess();
A writable resource sends the output to that stream. It must be backed by a real file (not php://memory
and the like):
$log = fopen('build.log', 'a');
Process::runExecutable('make', stdout: $log, stderr: $log);
false discards the output entirely (it goes to /dev/null, or NUL on Windows):
Process::runExecutable('noisy-tool', stderr: false);
Redirecting also keeps memory usage down: capturing into memory is convenient, but a process that prints gigabytes would use gigabytes of RAM, so write such output to a file.
Environment Variables
The $env parameter sets the environment variables the process will see. Leave it as null (the
default) to inherit the current process's environment, or pass an array to set them yourself:
// the current environment plus one extra variable
$process = Process::runExecutable('printenv', ['MY_VAR'], env: ['MY_VAR' => '123'] + getenv());
// a completely empty environment
$process = Process::runExecutable('some-tool', env: []);
Working Directory
The $directory parameter sets the directory the process starts in (by default it's the current one):
$process = Process::runExecutable('git', ['status'], directory: '/path/to/repo');
Time Limit
The $timeout parameter (in seconds, 60 by default) caps how long you'll wait for the process. If the
limit is reached while you are waiting for it or reading its output, the process is killed and
Nette\Utils\ProcessTimeoutException is thrown. Pass null to remove the limit:
$process = Process::runExecutable('slow-tool', timeout: 5.0);
try {
$process->wait();
} catch (Nette\Utils\ProcessTimeoutException $e) {
echo 'The tool took too long and was terminated.';
}
The limit is only checked while you're inside wait(), getExitCode(), the output getters, or
consume*(). A process you start and then never wait on is not killed by it.
Stopping a Process
terminate(): void
Kills the process immediately if it's still running; does nothing if it has already finished:
$process = Process::runExecutable('server');
// ...
$process->terminate();
A process is also terminated automatically when its Process object is destroyed (for instance, goes out of scope)
before it has finished.
getPid(): ?int
Returns the operating-system process ID (PID) while the process is running, or null once it has finished:
$pid = $process->getPid();
When Something Goes Wrong
Errors are always reported by throwing an exception, never by a return value:
Nette\Utils\ProcessFailedException |
the process could not be started, or ensureSuccess() was called and the exit code wasn't 0 |
Nette\Utils\ProcessTimeoutException |
the $timeout limit was exceeded |
Nette\InvalidArgumentException |
an invalid value was passed as $stdin, $stdout, or $stderr |
Nette\IOException |
a file given as $stdout or $stderr could not be opened |
Nette\InvalidStateException |
reading output that wasn't captured, or writing to a STDIN that's already closed |
Nette\NotSupportedException |
process piping was attempted on Windows |
ProcessFailedException and ProcessTimeoutException extend PHP's RuntimeException.