This post covers the Symfony2 process component and is the 11th post from our series on Symfony2 components.

The Symfony2 Process component, allows us to execute commands in sub-processes.

171847361_546785da09_z

The Process component does all the hard low-level work for you
when dealing with sub-processes

Installation

The recommended way of installing the component is through Composer:

{
    "require": {
        "symfony/process": "2.4.*"
    }
}

If you have never used Composer before check out our Composer 101 post.

The component

The Process component provides an object-oriented abstraction on top of proc_* functions to execute independent processes from PHP.

For example, to list the files and directories in the directory where the current PHP script is located:


use Symfony\Component\Process\Process;

$process = new Process('ls -lh ' . escapeshellarg(__DIR__));
$process->run();

// executes after the command finishes
if (!$process->isSuccessful()) {
    throw new \RuntimeException($process->getErrorOutput());
}

echo $process->getOutput();

The example is quite self-explanatory, but let’s take a look and go step-by-step to see what happened exactly. First, we create a Process instance, which is the main class of the component. Besides passing the command we want to execute, it is also possible to pass the working directory, environment variables or a timeout.

function __construct($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array())

As soon as we call the run() method, the command is executed and the PHP interpreter waits until the command finishes. In case that the execution was not successful (exit code different than 0), it throws an exception. If the execution was successful, it simply prints out the command output.

The output of the script would be similar to this:

total 64
-rw-r--r--  1 raul  raul   374B Apr 14 09:14 composer.json
-rw-r--r--  1 raul  raul    22K Apr 14 09:15 composer.lock
-rw-r--r--  1 raul  raul   321B Apr 14 11:32 ex1.php
drwxr-xr-x  8 raul  raul   272B Apr 14 09:15 vendor

Exit codes

The process class provides a few methods to deal with exit codes. We can even get the exit code as a text, as the Process class maintains a map of common exit codes:

$processA = new Process('ls -lh');
$processA->run();

// int(0) string(2) "OK"
var_dump($processA->getExitCode(), $processA->getExitCodeText());

$processB = new Process('foo');
$processB->run();

// int(127) string(17) "Command not found"
var_dump($processB->getExitCode(), $processB->getExitCodeText());

The exit code is also used to determine if a command was executed successfully, by the isSuccessful() method, which returns true if the exit code is 0 and false otherwise.

Long Running Processes

When we have to deal with long running processes things tend to get a little trickier, as we have take into account things like timeouts, incremental outputs, responsiveness, and signals. The Process class provides ways to make these problems manageable.

Timeouts

There are two available timeouts: process timeout (max runtime) and process idle timeout (max. time since last output). In the following code, as the ping command in Unix systems runs infinitely (unless we specify the “-c” option), a ProcessTimedOutException exception will be thrown after 10 seconds:

$process = new Process('ping example.com');
$process->setTimeout(10);
$process->run();

This would not be true with the idle timeout, as most of the time the ping command outputs new information in less than 10 seconds. In this case, the process will probably run until exceeds the memory_limit setting:

$process = new Process('ping example.com');
$process->setTimeout(null); // disable "normal" timeout
$process->setIdleTimeout(10);
$process->run();

Outputs

In long running processes we need some sort of “real time” output so the user perceives that the process is still running and is not dead. There are two ways to do this: outputting the command output as soon as it gets available, or printing some “loading” or “in progress” message. Let’s see an example of both approaches using again the ping command:

In the following example, we pass PHP callable to run whenever there is some output available on STDOUT (standard output) or STDERR (standard error). Each time there is output available, we print a dot so the user knows the command did not hang and is still running.

$process = new Process('ping -c 5 example.com');
echo 'Executing';
$process->run(function ($type) {
    if (Process::OUT === $type) {
        echo '.';
    }
});

It is also possible to print the command output as soon as it gets available defining a second parameter in the callable. This time, the script will print the command output as soon as it gets available:

$process = new Process('ping -c 5 example.com');
$process->run(function ($type, $buffer) {
    if (Process::ERR === $type) {
        echo $buffer;
    }
});

This can be done also with the getIncrementalOutput() and getIncrementalErrorOutput() methods, that returns only the new output since the last call.

$process = new Process('ping -c 5 example.com');
$process->start();

while ($process->isRunning()) {
    echo $process->getIncrementalOutput();
}

It is recommended to add a small delay in the while loop if we can afford it to reduce the number of calls to getIncrementalOutput().

Signals

Signals are asynchronous notifications sent to a process to notify of an event that occurred. Using signals we can for instance stop asynchronous processes using the signal() method:

$process = new Process('ping -c 50 example.com');
$process->start();
sleep(3);
$process->signal(SIGKILL);

The ping command is “killed” after 3 seconds. The SIGKILL constant is defined in PCNTL.

PIDs

A PID is a number to temporarily uniquely identify a process. We can get the PID of a running process with the getPid() method:

$process = new Process('ls -lh');
$process->start();

var_dump($process->getPid()); // int(78316)

Executing PHP Code

The component provides the class PhpProcess (which extends from Process), to execute PHP code in isolation. That means that it is run in a different process so no variables or open resources are shared between them.

use Symfony\Component\Process\PhpProcess;

$process = new PhpProcess('<!--?php echo "Hello world!";'); $process--->run();

echo $process->getOutput();

It will print out “Hello world!”.

Under the hood

If you are following the posts of this series you know that we always like to dive a little bit deeper and find out how the component is made internally. Usually, the official documentation for Symfony components is excellent so we try to give back to the community by explaining them in a different way and trying to share roughly how they work internally.

Internally, the Process class makes use of the proc_open() function to execute a command and open file pointers for input/output. The proc_open() function is not straightforward to use, as it needs a descriptor specification.

The descriptor specification is an indexed array to tell the function how we want to handle stdin, stdout and stderr. By default, in the component, pipes are used, but it can be configured to use a file for stdout instead. These are the parameters that proc_open() receives when executing “ls -lh”:

string(6) "ls -lh"
array(3) {
  [0] =>
  array(2) {
    [0] => string(4) "pipe"
    [1] => string(1) "r"
  }
  [1] =>
  array(2) {
    [0] => string(4) "pipe"
    [1] => string(1) "w"
  }
  [2] =>
  array(2) {
    [0] => string(4) "pipe"
    [1] => string(1) "w"
  }
}
array(0) {
}
string(31) "/Users/raulfraile/servergrove/process"
NULL
array(2) {
  'suppress_errors' => bool(true)
  'binary_pipes' => bool(true)
}

The option “suppress_errors” is only for Windows systems and suppresses errors generated by the proc_open() function, while “binary_pipes” forces to open pipes in binary mode, instead of using the usual stream_encoding.

The Process component is one of the oldest in the Symfony framework, it is quite interesting to view the transformation it went through over 4 years, take a look at how simple it was (254 LoC) and how much more complete it became (1446 LoC). This is a big reason why it is great to reuse well developed and tested libraries.

Finally, as a curiosity, the PhpProcess class, which is used to execute PHP code, already supports HHVM. The PhpExecutableFinder class, used to find the PHP binary, checks whether HHVM is being used by reading the HHVM_VERSION constant, only available when HHVM is the current engine.

Who’s using it?

More info

Photo: Work in progress, by Stefano Mortellaro