draw/process
draw/process is a Laravel/PHP package for running and managing external processes. It helps you start commands, capture output, handle errors, and control execution in a clean API—useful for queues, build tasks, and integrations that need shell tools.
Installation
composer require draw/process
First Use Case
Replace a native Symfony Process call with the package’s enhanced version:
use Draw\Process\Process;
$process = new Process(['php', 'artisan', 'queue:work', '--once']);
$process->run();
if (!$process->isSuccessful()) {
Log::error('Queue worker failed: ' . $process->getErrorOutput());
}
Where to Look First
src/Process.php: Core class extending Symfony’s Process with Laravel-friendly methods.tests/: Example test cases for edge cases (timeouts, output streaming).README.md: Basic usage snippets (if available; otherwise, inspect Process class docs).// In a Laravel Job
public function handle() {
$process = new Process(['python', 'scripts/process_data.py', '--input', $this->inputFile]);
$process->setTimeout(300); // 5-minute timeout
$process->run();
if ($process->isTimedOut()) {
$this->release(60); // Retry after 1 minute
} elseif (!$process->isSuccessful()) {
$this->fail($process->getErrorOutput());
}
}
// In a Livewire/Inertia component
$process = new Process(['ffmpeg', '-i', 'input.mp4', 'output.mp4']);
$process->run(function ($type, $buffer) {
$this->emit('process-output', [
'type' => $type === Process::OUT ? 'stdout' : 'stderr',
'data' => $buffer,
]);
});
// Chain commands (e.g., build + test)
$buildProcess = new Process(['npm', 'run', 'build']);
$testProcess = new Process(['npm', 'test']);
$buildProcess->run();
if ($buildProcess->isSuccessful()) {
$testProcess->run();
// Handle test results...
}
// Custom Artisan command
protected function execute(InputInterface $input, OutputInterface $output) {
$process = new Process(['php', 'artisan', 'migrate', '--env=testing']);
$process->setOutput($output); // Stream to Artisan's output
$process->run();
}
// Use config to switch commands per environment
$command = config('process.commands.backup');
$process = new Process(explode(' ', $command));
$process->run();
Service Provider Binding
Extend Laravel’s container to auto-resolve Process:
// app/Providers/AppServiceProvider.php
public function register() {
$this->app->bind(Process::class, function () {
return new \Draw\Process\Process([]);
});
}
Logging Decorator
Wrap Process to log all executions:
class LoggedProcess extends Process {
public function run($callback = null) {
Log::info('Running process: ' . $this->getCommandLine());
return parent::run($callback);
}
}
Queue Job Wrapper Create a reusable job for subprocesses:
namespace App\Jobs;
use Draw\Process\Process;
use Illuminate\Bus\Queueable;
class RunProcessJob implements Queueable {
use Dispatchable, InteractsWithQueue;
public function __construct(
public array $command,
public int $timeout = 300,
) {}
public function handle() {
$process = new Process($this->command);
$process->setTimeout($this->timeout);
$process->run();
if (!$process->isSuccessful()) {
throw new \RuntimeException($process->getErrorOutput());
}
}
}
Testing
Mock Process in unit tests:
$mockProcess = Mockery::mock(Process::class);
$mockProcess->shouldReceive('run')->andReturn(null);
$mockProcess->shouldReceive('isSuccessful')->andReturn(true);
$this->app->instance(Process::class, $mockProcess);
Silent Process Failures
run() returns null on success but doesn’t throw exceptions.isSuccessful() or use mustRun():
$process->mustRun(); // Throws \RuntimeException on failure
Resource Leaks in Long-Running Processes
terminate() or use Process::terminateAll():
register_shutdown_function(function () {
Process::terminateAll();
});
Environment Variable Pollution
$process->setEnv(['APP_ENV' => 'testing']); // Avoid `getenv()` leaks
Cross-Platform Path Issues
/tmp/file.log) fail on Windows.storage_path() or sys_get_temp_dir():
$process->setCommandLine([
'touch', storage_path('app/temp/file.log')
]);
Timeout Misconfiguration
sleep 60 with setTimeout(10)).300 seconds for long tasks).Output Buffering
tail -f logs).$process->run(function ($type, $buffer) {
file_put_contents('log.txt', $buffer, FILE_APPEND);
});
Inspect Process State
$process->run();
echo "Exit Code: " . $process->getExitCode();
echo "Output: " . $process->getOutput();
echo "Error: " . $process->getErrorOutput();
Enable Debug Logging
$process->setDebug(true); // Logs command execution to storage/logs/laravel.log
Check for Zombie Processes
ps aux | grep "php artisan" # Manually inspect running processes
Validate Command Line
echo $process->getCommandLine(); // Verify the exact command being run
Custom Process Decorators
Extend Process to add domain-specific logic:
class DatabaseBackupProcess extends Process {
public function __construct() {
parent::__construct(['mysqldump', '-u', 'root', 'database']);
}
public function backupTo(string $path) {
$this->setCommandLine([
'mysqldump', '-u', 'root', 'database', '>', $path
]);
}
}
Hooks for Pre/Post Execution
$process = new Process(['git', 'pull']);
$process->onStart(function () {
Log::info('Starting git pull...');
});
$process->onEnd(function ($exitCode) {
Log::info("Git pull exited with code: {$exitCode}");
});
Laravel Event Integration Dispatch events for process lifecycle:
$process = new Process(['composer', 'install']);
event(new ProcessStarting($process));
$process->run();
event(new ProcessEnded($process));
Symfony Process Version Conflicts
symfony/process:^5.4 but Laravel uses 6.x.composer.json:
"require": {
"symfony/process": "^6.0"
},
"extra": {
"laravel": {
"dont-discover": ["draw/process"]
}
}
Laravel’s Native Process Facade
Process facade from illuminate/process.use
How can I help you explore Laravel packages today?