Process: running external programs from PHP without the pain
Running an external program from PHP and reading what it printed?
shell_exec('git pull') and you're done. But that only covers the
simplest case. The moment you need more — the exit code, errors kept separate
from the regular output, a user-supplied argument passed safely, a timeout —
shell_exec() runs out of road. And proc_open(), which
can do all of it, has an API so unwelcoming that most people just avoid it.
Nette Utils 4.1.4 adds the Nette\Utils\Process class that fills
that gap between the two. You start a process, then you ask it questions: is it
still running? what did it print? how did it end? And it takes care of the
pipes, the polling loops, and the differences between operating systems
for you.
How to Execute a Command and Get the Exit Code in PHP
Want to run git pull and know whether it passed?
use Nette\Utils\Process;
Process::runExecutable('git', ['pull'])->ensureSuccess();
That single line runs git pull, waits for it to finish, and
throws ProcessFailedException if the exit code wasn't zero. No
juggling exec() and $retval, no
is_resource(), no loop.
And when you want the output?
$process = Process::runExecutable('git', ['log', '-1', '--format=%H']);
echo $process->getStdOutput(); // standard output
echo $process->getStdError(); // error output, separately
echo $process->getExitCode(); // 0
getStdOutput() waits for the program to finish and returns
everything it wrote to standard output. The error output you get separately,
which shell_exec() can't do. The exit code too. And unlike
PHP's native functions, the class doesn't return false on errors,
it throws exceptions, so you won't miss a failure. (Need to send the program
some input? Pass stdin: 'data', or null and feed it
incrementally via writeStdInput().)
runExecutable vs. runCommand (Preventing Shell Injection)
The class offers two ways to start a process, and the difference is not cosmetic.
runExecutable() runs one specific program with a list of
arguments. The arguments are handed to the program directly. No shell ever
touches them, so you don't have to escape anything and, more importantly,
there's no risk of shell injection:
$file = $_GET['file']; // could be anything, even '; rm -rf /'
$process = Process::runExecutable('wc', ['-l', $file]); // perfectly safe
runCommand(), on the other hand, hands the whole string to the
system shell (/bin/sh, or cmd.exe on Windows).
That gives you pipes, redirects, variable expansion, in short everything you'd
expect from a shell:
$process = Process::runCommand('git log --oneline | head -n 20');
The price for the convenience: the shell parses that string, so never put
unescaped outside input into it. The rule is simple: anything involving user
input goes through runExecutable().
Streaming and Reading Process Output in Real-Time
You're running something that takes minutes (npm install,
composer update, a build) and you want to see the progress, not
wait for the final dump. Pass wait() a callback:
$process = Process::runExecutable('npm', ['install']);
$process->wait(function (string $stdOut, string $stdErr) {
echo $stdOut;
fwrite(STDERR, $stdErr);
});
The callback is invoked every time new output arrives. It gets whatever has
been added to standard and error output since the previous call. No
stream_select(), no switching pipes to non-blocking mode, no risk
of a deadlock when the program spits out more than fits in the pipe buffer. The
class handles all of that.
Don't want a callback? Pull the output yourself with
consumeStdOutput(), which returns the chunk added since the
previous call:
while ($process->isRunning()) {
echo $process->consumeStdOutput();
usleep(100_000);
}
echo $process->consumeStdOutput(); // grab the rest
Writing Large Process Output Directly to a File
You don't always want the output in memory. Send a large database dump straight to a file:
Process::runExecutable('mysqldump', ['mydb'], stdout: 'backup.sql')->ensureSuccess();
The stdout and stderr parameters take a filename, a
writable resource, false (discard), or null (capture
into memory, the default). Handy when a process pours out gigabytes that you
don't want sitting in RAM.
How to Set a Timeout for External Processes in PHP
An external program can get stuck. The class has the $timeout
parameter for that (60 seconds by default):
try {
Process::runExecutable('slow-tool', timeout: 5.0)->wait();
} catch (Nette\Utils\ProcessTimeoutException $e) {
echo 'It took too long and the process was terminated.';
}
When the limit expires (it's checked while you wait for the process or read
from it), the process is killed and ProcessTimeoutException is
thrown. And it's killed for real: SIGKILL on Unix, taskkill /F on
Windows. No SIGTERM that a stubborn process could ignore and leave you hanging
forever in proc_close(). The terminate() method works
just as decisively, and a process is also terminated automatically when its
Process object goes away.
Piping Processes Together in PHP (stdin / stdout)
Want to connect one process's output to another's input, exactly like a
| pipe in the shell, but without a shell? Pass a
Process as the $stdin:
$producer = Process::runExecutable('cat', ['big.log']);
$consumer = Process::runExecutable('grep', ['error'], stdin: $producer);
echo $consumer->getStdOutput();
And you can chain as many as you like: a | b | c. On Windows
this is the one thing that doesn't work; there, capture the first
process's output and pass it to the next one as a string. Otherwise the class
behaves the same on Linux, macOS and Windows, handling the differences you'd
rather not know about under the hood.
Process is in Nette Utils 4.1.4 (documentation). Give it a try
and let us know. And proc_open()? You won't have to touch it.
Sign in to submit a comment