Process: running external programs from PHP without the pain

about an hour ago by David Grudl  

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.

David Grudl Founder of Uměligence and creator of Nette Framework, the popular PHP framework. Since 2021, he's been fully immersed in artificial intelligence, teaching practical AI applications. He discusses weekly tech developments on Tech Guys with his co-hosts and writes for phpFashion and La Trine. He believes AI isn't science fiction—it's a practical tool for improving life today.