symfony/process
Symfony Process component runs external commands in separate processes with robust control over arguments, environment, timeouts, and working directory. Capture stdout/stderr, stream live output, manage input, and handle exit codes reliably across platforms.
Installation:
composer require symfony/process
For Laravel, no additional setup is required—it integrates seamlessly with the framework.
First Use Case: Execute a simple command and capture output:
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
$process = new Process(['ls', '-la']);
$process->run();
// Check if the process ran successfully
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
echo $process->getOutput();
Where to Look First:
Artisan commands (e.g., php artisan make:command) for integrating subprocesses into CLI workflows.Process facade for dependency injection in Laravel services.$process = new Process(['git', 'pull']);
$process->run();
if ($process->isSuccessful()) {
echo "Git pull succeeded!";
} else {
echo "Error: " . $process->getErrorOutput();
}
Useful for long-running commands (e.g., docker-compose up or npm run build):
$process = new Process(['docker-compose', 'up']);
$process->start();
// Stream output line by line
foreach ($process as $type => $output) {
if (Process::ERR === $type) {
echo 'ERR > ' . $output;
} else {
echo 'OUT > ' . $output;
}
}
$process = new Process(['php', 'script.php']);
$process->setEnvironment([
'DB_HOST' => 'localhost',
'DEBUG' => 'true',
]);
$process->setWorkingDirectory(storage_path('app'));
$process->run();
Prevent hanging processes:
$process = new Process(['sleep', '10']);
$process->setTimeout(5); // Fail after 5 seconds
$process->run();
if (!$process->isSuccessful()) {
$process->stop(); // Terminate the process
}
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
class RunBackupCommand extends Command
{
protected $signature = 'backup:run';
protected $description = 'Run database backup';
public function handle()
{
$process = new Process(['mysqldump', '-u', 'root', '-p', 'database']);
$process->run();
if (!$process->isSuccessful()) {
$this->error($process->getErrorOutput());
return 1;
}
$this->info('Backup completed!');
}
}
Offload subprocess execution to a queue worker (e.g., RunProcessMessage in Symfony Messenger):
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Process\RunProcessMessage;
$bus->dispatch(new RunProcessMessage([
'command' => 'php',
'arguments' => ['artisan', 'queue:work'],
'timeout' => 3600,
]));
Dependency Injection:
Laravel’s service container automatically resolves Process instances. Bind custom configurations in AppServiceProvider:
$this->app->bind(Process::class, function ($app) {
return new Process(['default-command']);
});
Logging Process Output: Use Laravel’s logging facade to capture subprocess output:
$process = new Process(['some-command']);
$process->run();
\Log::info('Command output:', [
'stdout' => $process->getOutput(),
'stderr' => $process->getErrorOutput(),
'exit_code' => $process->getExitCode(),
]);
Cross-Platform Path Handling:
Use Laravel’s str() helper or Path facade to ensure paths are OS-agnostic:
use Illuminate\Support\Str;
$process = new Process(['node', Str::of(storage_path('scripts/build.js'))]);
Retry Logic for Transient Failures:
Combine with Laravel’s retry helper or a custom retry mechanism:
use Illuminate\Support\Facades\Retry;
Retry::times(3)->attempt(function () {
$process = new Process(['git', 'pull']);
$process->run();
if (!$process->isSuccessful()) {
throw new \RuntimeException('Git pull failed');
}
});
Windows Environment Variable Limits:
InvalidArgumentException.setEnvironment() calls.$env = [];
foreach ($largeEnvVars as $key => $value) {
if (strlen($key . '=' . $value) > 32000) {
throw new \RuntimeException("Environment variable too large for Windows");
}
$env[$key] = $value;
}
$process->setEnvironment($env);
Broken Pipes on stdin:
stdin pipe (e.g., when the subprocess closes unexpectedly) can cause PHP to hang.ignore_stdin() or handle Process::ERR output gracefully.$process = new Process(['cat']);
$process->ignoreStdin(); // Ignore stdin if not needed
PTY Mode and Mixed Output:
stdout and stderr can be mixed, making parsing difficult.stderr:
$process = new Process(['some-command'], null, [
'pipe_stderr' => true, // Separate stderr pipe
]);
Signal Handling Across Platforms:
SIGKILL) behaves differently on Windows vs. Unix.setTimeout() for cross-platform timeouts instead of signals:
$process->setTimeout(10); // Fails after 10 seconds
Command Injection Risks:
Process constructor with an array of arguments, not a shell string:
// UNSAFE: Vulnerable to shell injection
$process = new Process('ls ' . $userInput);
// SAFE: Array arguments
$process = new Process(['ls', $userInput]);
Resource Leaks:
run(), start(), or stop() can leave processes running.__destruct() to ensure cleanup or wrap in a try-finally block:
$process = new Process(['sleep', '10']);
try {
$process->start();
// ... stream output ...
} finally {
$process->stop();
}
Enable Verbose Output:
Use setVerbose() to debug process execution:
$process = new Process(['some-command']);
$process->setVerbose();
$process->run();
Check Exit Codes:
0: Success.>0: Failure (check getErrorOutput()).127: Command not found.137: Process killed (e.g., by SIGKILL).Log Process Metadata: Log the full command, environment, and working directory for debugging:
\Log::debug('Process executed', [
'command' => $process->getCommandLine(),
'env' => $process->getEnvironment(),
'cwd' => $process->getWorkingDirectory(),
]);
Use getOutputFromFunction() for Testing:
Simulate subprocess output in tests:
$output = Process::getOutputFromFunction(function () {
return "test output\n";
});
How can I help you explore Laravel packages today?