cocur/background-process
Run shell commands as detached background processes from PHP so they keep running after the request/script ends. Start jobs, optionally get PID, poll if running, and stop them (Unix). Basic Windows support for launching only.
Start by installing the package via Composer:
composer require cocur/background-process
For a quick test, run a simple command in the background:
use Cocur\BackgroundProcess\BackgroundProcess;
$process = new BackgroundProcess('sleep 5');
$process->run(); // Runs instantly; command executes in background
Replace a blocking exec() call in a Laravel route or Artisan command:
// Before (blocks HTTP request)
exec('php artisan optimize:images {storage_path}');
// After (non-blocking)
$process = new BackgroundProcess('php artisan optimize:images ' . storage_path());
$process->run();
return response()->json(['message' => 'Task started in background']);
sleep, echo) to verify behavior.$process = new BackgroundProcess('php script.php --long-running');
$process->run(); // Detaches immediately
$pid = $process->getPid();
// Store $pid in DB for later reference
if ($process->isRunning()) {
// Task still active
}
$process->stop(); // Sends SIGTERM
use Cocur\BackgroundProcess\BackgroundProcess;
class OptimizeImagesCommand extends Command {
protected $signature = 'images:optimize {path}';
public function handle() {
$process = new BackgroundProcess("php artisan optimize:images {$this->argument('path')}");
$process->run();
$this->info("Optimization started in background (PID: {$process->getPid()})");
}
}
use Cocur\BackgroundProcess\BackgroundProcess;
Route::post('/generate-report', function (Request $request) {
$process = new BackgroundProcess('php artisan reports:generate ' . $request->input('type'));
$process->run();
// Store PID in DB for tracking
DB::table('background_jobs')->insert([
'command' => $process->getCommand(),
'pid' => $process->getPid(),
'created_at' => now(),
]);
return response()->json(['status' => 'queued']);
});
use Cocur\BackgroundProcess\BackgroundProcess;
class SendWelcomeEmail implements ShouldQueue {
public function handle() {
$process = new BackgroundProcess('php artisan emails:send welcome ' . auth()->id());
$process->run();
}
}
// Store PID in DB
DB::table('background_jobs')->insert([
'command' => $process->getCommand(),
'pid' => $process->getPid(),
'user_id' => auth()->id(),
]);
// Later, retrieve and stop
$job = DB::table('background_jobs')->find($jobId);
$process = BackgroundProcess::createFromPID($job->pid);
$process->stop();
Append command output to a log file:
$process = new BackgroundProcess('php script.php', null, null, null, null, '/path/to/logfile');
$process->run();
For managing multiple background tasks:
class BackgroundTaskManager {
private $processes = [];
public function add(string $command) {
$process = new BackgroundProcess($command);
$process->run();
$this->processes[$process->getPid()] = $process;
}
public function stopAll() {
foreach ($this->processes as $process) {
$process->stop();
}
$this->processes = [];
}
}
Windows Limitations:
getPid(), isRunning(), and stop() throw exceptions on Windows.Zombie Processes:
# Example cron entry (Linux)
* * * * * php /path/to/cleanup_stale_processes.php
// cleanup_stale_processes.php
$staleJobs = DB::table('background_jobs')
->where('updated_at', '<', now()->subHours(1))
->get();
foreach ($staleJobs as $job) {
$process = BackgroundProcess::createFromPid($job->pid);
if ($process->isRunning()) {
$process->stop();
}
DB::table('background_jobs')->where('id', $job->id)->delete();
}
Resource Exhaustion:
Stale PID Handling:
createFromPid() may return a process that already terminated.$process = BackgroundProcess::createFromPid($pid);
if (!$process->isRunning()) {
throw new \RuntimeException("Process {$pid} is not running");
}
Output Capture:
$process = new BackgroundProcess('php script.php', null, null, null, null, '/var/log/script.log');
Check Process Status:
$process = BackgroundProcess::createFromPid($pid);
if ($process->isRunning()) {
echo "Process is running (PID: {$process->getPid()})";
} else {
echo "Process not found or terminated";
}
Manual Process Inspection:
ps aux | grep <command> (Linux) or Task Manager (Windows) to verify processes.Logging PID:
file_put_contents(
storage_path('logs/background_pids.log'),
"{$process->getPid()}: {$process->getCommand()}\n",
FILE_APPEND
);
Command Construction:
;, &, |) in commands to prevent injection.$process = new BackgroundProcess('/full/path/to/script.php arg1 arg2');
Environment Variables:
putenv('APP_ENV=production');
$process = new BackgroundProcess('php script.php');
Working Directory:
chdir('/custom/directory');
$process = new BackgroundProcess('php script.php');
Custom Process Wrapper:
BackgroundProcess for additional features:
class LaravelBackgroundProcess extends BackgroundProcess {
public function __construct(string $command) {
parent::__construct($command);
$this->logPid();
}
protected function logPid() {
DB::table('background_jobs')->insert([
'command' => $this->getCommand(),
'pid' => $this->getPid(),
'created_at' => now(),
]);
}
}
Process Monitoring Middleware:
class BackgroundProcessMiddleware {
public function handle($request, Closure $next) {
if ($request->has('stop_job')) {
$process = BackgroundProcess::createFromPid($request->input('pid'));
$process->stop();
}
return $next($request);
}
}
Integration with Laravel Events:
Event::listen('user.registered', function ($user) {
$process = new BackgroundProcess("php artisan send-welcome-email {$user->id}");
$process->run();
});
How can I help you explore Laravel packages today?