titasgailius/terminal
titasgailius/terminal is a Laravel-friendly wrapper for running shell commands via Symfony Process. Build, execute, and chain commands fluently, capture output, handle errors/timeouts, and mock processes in tests for reliable command-line automation.
Installation
composer require titasgailius/terminal
For Laravel, no service provider is needed—use the facade or resolve the class directly.
First Command Execution
use Terminal\Command;
$output = Command::run('ls -la')->output();
echo $output;
Basic Error Handling
try {
$result = Command::run('ls /nonexistent')->output();
} catch (\Terminal\Exception\CommandFailedException $e) {
// Handle failure (exit code != 0)
echo "Command failed: " . $e->getMessage();
}
Terminal\Facades\Command (Laravel) or Terminal\Command (plain PHP).run() → Execute and return a Result object.output() → Capture stdout.errorOutput() → Capture stderr.exitCode() → Get exit code.Pattern: Chain methods for readability and reusability.
$output = Command::run('git pull')
->workingDirectory(storage_path('app'))
->env(['GIT_TERMINAL_PROMPT', '0'])
->timeout(30)
->output();
Use Case: Wrap repetitive commands in a helper:
function deploy() {
return Command::run('php artisan deploy')
->env(['DEPLOY_KEY', config('deploy.key')])
->timeout(120);
}
Pattern: Stream real-time output for CLI feedback.
Command::run('docker-compose up')
->stream(function ($type, $line) {
if ($type === 'stdout') {
echo "[LOG] $line\n";
} elseif ($type === 'stderr') {
echo "[ERR] $line\n";
}
});
Use Case: Log command output to a file:
$logFile = fopen(storage_path('logs/command.log'), 'a');
Command::run('php artisan queue:work')
->stream(function ($type, $line) use ($logFile) {
fwrite($logFile, "$type: $line\n");
});
Pattern: Extend Artisan commands with shell logic.
use Terminal\Command;
use Illuminate\Console\Command as ArtisanCommand;
class MyArtisanCommand extends ArtisanCommand {
protected function handle() {
$result = Command::run('php artisan migrate:fresh')
->output();
$this->info("Migration output:\n$result");
}
}
Use Case: Pre/post-command hooks:
Command::run('npm install')
->before(function () {
$this->info('Running npm install...');
})
->after(function ($result) {
$this->line("Exit code: {$result->exitCode()}");
});
Pattern: Mock commands in tests using Terminal\Mock\Command.
public function test_deploy_command() {
Command::shouldReceive('run')
->with('php artisan deploy')
->once()
->andReturn(new \Terminal\Result(0, 'Success', ''));
$this->assertTrue($this->deploy());
}
Use Case: Test error scenarios:
Command::shouldReceive('run')
->with('ls /invalid')
->andThrow(new \Terminal\Exception\CommandFailedException(1, 'Not found'));
$this->expectException(\RuntimeException::class);
$this->deploy();
Argument Escaping
// UNSAFE: Vulnerable to injection
Command::run('rm ' . $userInput);
Command::run('rm')->arg($userInput)->safe();
Timeouts
null (no timeout). Always set a timeout for long-running commands.
Command::run('php artisan queue:work')->timeout(60); // 60 seconds
Working Directory
workingDirectory() are resolved relative to the current PHP process, not the command’s perspective.
// May not work as expected
Command::run('pwd')->workingDirectory('../');
Command::run('pwd')->workingDirectory(realpath('../'));
Environment Variables
// Avoid unless necessary
Command::run('npm install')->env(['PATH', '/custom/path']);
Command::run('npm install')->env(['NODE_ENV', 'production']);
Inspect Raw Output
Use ->combinedOutput() to debug mixed stdout/stderr:
$combined = Command::run('ls /invalid')->combinedOutput();
$this->info($combined);
Exit Code Analysis Check exit codes for non-obvious failures:
$exitCode = Command::run('git status')->exitCode();
if ($exitCode === 128) {
$this->error('Command killed by signal.');
}
Real-Time Streaming Issues
->stream() with a buffer or log to a file.Custom Exceptions
Extend \Terminal\Exception\CommandFailedException for domain-specific errors:
class DeploymentFailedException extends CommandFailedException {}
Command Wrappers Create reusable command classes:
class GitCommand {
public function pull(string $repo) {
return Command::run('git pull origin main')
->workingDirectory($repo)
->safe();
}
}
Integration with Laravel Events Trigger events on command success/failure:
Command::run('php artisan optimize')
->after(function ($result) {
if ($result->successful()) {
event(new CommandSucceeded($result));
}
});
Parallel Commands
Use Terminal\Parallel for concurrent executions (requires PHP 7.4+):
use Terminal\Parallel;
Parallel::run([
Command::run('php artisan queue:work'),
Command::run('tail -f storage/logs/laravel.log'),
]);
How can I help you explore Laravel packages today?