Laravel Magazine
The Process Facade: Shelling Out to System Commands Safely and Testably

The Process Facade: Shelling Out to System Commands Safely and Testably

Eric Van Johnson ·

Sooner or later a Laravel app needs to shell out: converting a video, running ffmpeg, invoking a CLI tool, calling a script another team owns. The Process facade gives you a clean, fluent wrapper around Symfony's Process component, with a testing story that makes it painless to fake.

Running a command

use Illuminate\Support\Facades\Process;

$result = Process::run('git status');

$result->successful(); // bool
$result->output();     // stdout
$result->errorOutput(); // stderr
$result->exitCode();

Passing arguments safely

Never string-concatenate user input into a shell command. Pass an array instead and let Laravel handle escaping:

$result = Process::run(['convert', $inputPath, '-resize', '800x600', $outputPath]);

Timeouts and working directory

$result = Process::path(storage_path('app/exports'))
    ->timeout(120)
    ->run('php artisan export:generate');

If a command exceeds its timeout, Laravel throws a Symfony\Component\Process\Exception\ProcessTimedOutException, which you can catch to clean up partial output.

Streaming output in real time

For long-running commands where you want to react to output as it comes rather than waiting for completion:

Process::run('npm run build', function (string $type, string $output) {
    if ($type === 'err') {
        Log::warning($output);
    } else {
        Log::info($output);
    }
});

Running commands concurrently

Just like the HTTP client's connection pool, Process supports running multiple commands in parallel and waiting on all of them:

$results = Process::pool(function (\Illuminate\Process\Pool $pool) {
    $pool->as('thumbnails')->command('php artisan images:thumbnail 1');
    $pool->as('metadata')->command('php artisan images:extract-metadata 1');
})->start()->wait();

$results['thumbnails']->successful();
$results['metadata']->successful();

Throwing on failure

Process::run('composer validate')->throw();

throw() raises a ProcessFailedException if the command exits non-zero, which is convenient in deployment scripts and Artisan commands where a failed step should halt everything downstream.

Testing without actually running anything

This is where Process earns its keep over calling shell_exec() or Symfony's Process component directly. You can fake process execution entirely in tests:

use Illuminate\Support\Facades\Process;

Process::fake([
    'git status' => Process::result(output: 'nothing to commit'),
    'ffmpeg*' => Process::result(exitCode: 1, errorOutput: 'invalid codec'),
]);

Process::run('git status')->output(); // 'nothing to commit'

Process::assertRan('git status');

No test suite should actually be invoking ffmpeg or git on a CI runner if it can avoid it, and Process::fake() means you can test the branching logic around a command's success or failure without ever touching a real shell.

Why reach for this instead of exec()

The core value is consistency: one API for running, streaming, pooling, and testing shell commands, instead of a patchwork of shell_exec(), proc_open(), and ad hoc mocking. If your app talks to the command line at all, even just for one wkhtmltopdf call, Process is worth using instead of PHP's raw process functions.

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.