How to manage asynchronous process in PHP with Symfony 4

Recently I discovered the Process Class which is the perfect way to manage your child processes in a Symfony Command.

I work on a Open Source project to synchonize the results from vulnerability scanners like Tenable Nessus in a local database and so to be able to generate some advanced reports on vulnerabilities. The problem is the connection to the Nessus API (in REST) can be very time consuming and so, even if I connect through Symfony commands with Crontab, the duration of each command is an issue.

I observed than when I had to get several scans I wasn’t optimizing the multithread capacity of the Nessus server. For that, I have to start different processes asynchronously. Even if I program in Symfony for years now, I didn’t knew that this Process component was existing. And after reading the documentation, resolving my problem became a child’s play.

First you have to install the component as usual in Symfony 4 :

composer require symfony/process

You laready made the hardest part… Don’t forget to declare the use of the classes needed in your controller :

use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;

To start a process asynchronously, can’t be easier :

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

As I want to start some Symfony commands, I use this component to get the path of my PHP bin in the server. I get also the Project Path and after that I’m ready to launch Symfony command :

// Getting the path of PHP and of the project to fork multi threads
$phpBinaryFinder = new PhpExecutableFinder();
$phpBinaryPath = $phpBinaryFinder->find();
$projectRoot = $this->getContainer()->get('kernel')->getProjectDir();

Of course, with that component you can start several subprocesses, but what happened if your main command end before the child ? Well, logically you child processes will be terminated. So you have to be sure that all you child processes are ended before to end the root command with the wait method :

$process1->wait();

Here my final code :

foreach ($responseScans->body->scans as $scan) {
${'process' . $i} = new Process(array($phpBinaryPath, $projectRoot . '/bin/console', 'app:ScannerGetScan', $scan->id, $scan->name));
${'process' . $i}->setTimeout(9000);
${'process' . $i}->start();
$output->writeln('Starting thread process' . $i);
++$i;
}

// We wait for the termination of each of our process started
for ($j = 1; $j < $i; $j++) {
${'process' . $j}->wait();
$output->writeln('process' . $j . ' ended');
}

If you need to debug your child command, you can add this code, but keep in mind you killing the asynchronous mode by doing that :

${'process' . $i}->wait(function ($type, $buffer) {
if (Process::ERR === $type) {
echo 'ERR > '.$buffer;
} else {
echo 'OUT > '.$buffer;
}
});

Hope this article will help you and than next time you’ll have to program some batch processes you’ll think to Symfony to reach your goal !

My twitter feed : @protectyourit