“Code is like humor. When you have to explain it, it’s bad.”
— Cory House
Key Takeaways
- Progress Bars: Basic, advanced formatting, multiple progress bars with real-time updates
- Interactive Prompts: User input, choices, validation, and complete menu systems
- Background Processing: Queue integration, parallel processing, and long-running commands with signal handling
Index
- Basic Command Structure
- Basic Command Template
- Progress Bars
- Advanced Progress Bar with Custom Format
- Multiple Progress Bars
- Interactive Prompts
- Choice Selection
- Advanced Input Validation
- Interactive Menu System
- Background Processing
- Parallel Processing with Process Pools
- Long-Running Command with Signal Handling
- Advanced Features
- Configuration and Environment Detection
- Error Handling and Retry Logic
- Best Practices
- Registering Commands
- Stats
- Interesting Facts
- FAQ’s
- Conclusion
1. Basic Command Structure
Creating a New Command
php artisan make:command ProcessDataCommand
2. Basic Command Template
<?php namespace App\Console\Commands; use Illuminate\Console\Command; class ProcessDataCommand extends Command { protected $signature = 'data:process {--batch-size=100 : Number of records to process at once} {--force : Force processing without confirmation} {file? : Optional file path}'; protected $description = 'Process data with advanced features'; public function handle() { $this->info('Starting data processing...'); // Command logic here return Command::SUCCESS; } }
3. Progress Bars
Basic Progress Bar
public function handle() { $items = collect(range(1, 100)); $bar = $this->output->createProgressBar($items->count()); $bar->start(); foreach ($items as $item) { // Process item sleep(1); // Simulate work $bar->advance(); } $bar->finish(); $this->newLine(2); $this->info('Processing complete!'); }
4. Advanced Progress Bar with Custom Format
public function handle() { $items = User::chunk(100); $totalUsers = User::count(); // Custom progress bar format $bar = $this->output->createProgressBar($totalUsers); $bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s% - %message%'); $bar->setMessage('Starting...'); $bar->start(); $processed = 0; User::chunk(100, function ($users) use ($bar, &$processed) { foreach ($users as $user) { // Process user $this->processUser($user); $bar->setMessage("Processing user: {$user->email}"); $bar->advance(); $processed++; } }); $bar->setMessage('Complete!'); $bar->finish(); $this->newLine(2); $this->info("Processed {$processed} users successfully."); }
5. Multiple Progress Bars
public function handle() { $steps = [ 'users' => User::count(), 'orders' => Order::count(), 'products' => Product::count(), ]; foreach ($steps as $type => $count) { $this->info("Processing {$type}..."); $bar = $this->output->createProgressBar($count); $bar->start(); $this->{"process" . ucfirst($type)}($bar); $bar->finish(); $this->newLine(); } $this->info('All processing complete!'); } private function processUsers($bar) { User::chunk(50, function ($users) use ($bar) { foreach ($users as $user) { // Process user logic $bar->advance(); } }); }
6. Interactive Prompts
Basic User Input
public function handle() { // Simple input $name = $this->ask('What is your name?'); // Input with default value $email = $this->ask('What is your email?', 'user@example.com'); // Secret input (password) $password = $this->secret('Enter password'); // Confirmation $confirmed = $this->confirm('Do you want to continue?', true); if (!$confirmed) { $this->error('Operation cancelled.'); return Command::FAILURE; } $this->info("Hello {$name}! Email: {$email}"); }
7. Choice Selection
public function handle() { // Single choice $environment = $this->choice( 'Which environment?', ['local', 'staging', 'production'], 0 // default index ); // Multiple choice $features = $this->choice( 'Select features to enable (separate multiple with comma)', ['caching', 'logging', 'debugging', 'monitoring'], null, null, true // multiple selection ); $this->info("Environment: {$environment}"); $this->info('Features: ' . implode(', ', (array) $features)); }
8. Advanced Input Validation
public function handle() { $email = $this->askWithValidation( 'Enter email address', 'required|email', 'Please enter a valid email address' ); $age = $this->askWithValidation( 'Enter your age', 'required|integer|min:18|max:120', 'Age must be between 18 and 120' ); $this->info("Email: {$email}, Age: {$age}"); } private function askWithValidation($question, $rules, $errorMessage) { do { $input = $this->ask($question); $validator = validator(['input' => $input], ['input' => $rules]); if ($validator->fails()) { $this->error($errorMessage); $this->error($validator->errors()->first('input')); $input = null; } } while (is_null($input)); return $input; }
9. Interactive Menu System
public function handle() { do { $this->showMenu(); $choice = $this->choice('Select an option', [ 'process_users' => 'Process Users', 'process_orders' => 'Process Orders', 'generate_report' => 'Generate Report', 'exit' => 'Exit' ]); switch ($choice) { case 'process_users': $this->processUsers(); break; case 'process_orders': $this->processOrders(); break; case 'generate_report': $this->generateReport(); break; case 'exit': $this->info('Goodbye!'); return Command::SUCCESS; } $this->ask('Press Enter to continue...'); } while (true); } private function showMenu() { $this->newLine(); $this->info('=== Data Processing Menu ==='); $this->newLine(); }
10. Background Processing
Queue Integration
public function handle() { $batchSize = $this->option('batch-size') ?? 100; $totalRecords = User::count(); if (!$this->confirm("Queue {$totalRecords} records for processing?")) { return Command::FAILURE; } $bar = $this->output->createProgressBar(ceil($totalRecords / $batchSize)); $bar->start(); User::chunk($batchSize, function ($users) use ($bar) { ProcessUsersBatch::dispatch($users->pluck('id')->toArray()); $bar->advance(); }); $bar->finish(); $this->newLine(); $this->info('All batches queued successfully!'); // Monitor progress if ($this->confirm('Monitor queue progress?')) { $this->monitorQueueProgress(); } } private function monitorQueueProgress() { $this->info('Monitoring queue progress (Ctrl+C to stop)...'); while (true) { $pending = \DB::table('jobs')->count(); $failed = \DB::table('failed_jobs')->count(); $this->line("Pending: {$pending}, Failed: {$failed}"); if ($pending === 0) { $this->info('All jobs completed!'); break; } sleep(5); } }
11. Parallel Processing with Process Pools
use Symfony\Component\Process\Process; public function handle() { $maxProcesses = $this->option('max-processes') ?? 4; $items = $this->getItemsToProcess(); $chunks = $items->chunk(ceil($items->count() / $maxProcesses)); $processes = collect(); foreach ($chunks as $index => $chunk) { $tempFile = storage_path("app/temp_chunk_{$index}.json"); file_put_contents($tempFile, $chunk->toJson()); $process = new Process([ 'php', 'artisan', 'data:process-chunk', $tempFile ]); $process->start(); $processes->push($process); } // Monitor processes $bar = $this->output->createProgressBar($processes->count()); $bar->start(); while ($processes->contains(fn($p) => $p->isRunning())) { $completed = $processes->filter(fn($p) => !$p->isRunning())->count(); $bar->setProgress($completed); sleep(1); } $bar->finish(); $this->newLine(); // Check for failures $failed = $processes->filter(fn($p) => !$p->isSuccessful()); if ($failed->isNotEmpty()) { $this->error("Failed processes: " . $failed->count()); return Command::FAILURE; } $this->info('All processes completed successfully!'); }
12. Long-Running Command with Signal Handling
public function handle() { // Handle graceful shutdown pcntl_signal(SIGTERM, [$this, 'handleShutdown']); pcntl_signal(SIGINT, [$this, 'handleShutdown']); $this->running = true; $this->info('Starting long-running process... (Ctrl+C to stop gracefully)'); while ($this->running) { pcntl_signal_dispatch(); // Do work $this->processNextBatch(); // Prevent CPU overload sleep(1); } $this->info('Process stopped gracefully.'); } private $running = true; public function handleShutdown($signal) { $this->newLine(); $this->warn('Received shutdown signal. Finishing current batch...'); $this->running = false; }
13. Advanced Features
Command Dependencies and Chaining
public function handle() { $dependencies = [ 'migrate:fresh' => 'Resetting database', 'db:seed' => 'Seeding database', 'cache:clear' => 'Clearing cache', ]; foreach ($dependencies as $command => $description) { $this->info($description . '...'); $result = $this->call($command); if ($result !== 0) { $this->error("Failed to execute: {$command}"); return Command::FAILURE; } $this->info('✓ ' . $description . ' completed'); } // Main processing $this->processMainTask(); }
14. Configuration and Environment Detection
public function handle() { // Environment checks if (app()->environment('production') && !$this->option('force')) { if (!$this->confirm('Running in production. Continue?')) { return Command::FAILURE; } } // Memory and time limits ini_set('memory_limit', '1G'); set_time_limit(0); // Configuration $config = [ 'batch_size' => $this->option('batch-size') ?? config('processing.batch_size', 100), 'max_retries' => config('processing.max_retries', 3), 'timeout' => config('processing.timeout', 300), ]; $this->info('Configuration: ' . json_encode($config, JSON_PRETTY_PRINT)); return $this->processWithConfig($config); }
15. Error Handling and Retry Logic
public function handle() { $items = $this->getItemsToProcess(); $maxRetries = 3; $failed = collect(); $bar = $this->output->createProgressBar($items->count()); $bar->start(); foreach ($items as $item) { $success = $this->processItemWithRetry($item, $maxRetries); if (!$success) { $failed->push($item); } $bar->advance(); } $bar->finish(); $this->newLine(); if ($failed->isNotEmpty()) { $this->error("Failed to process {$failed->count()} items"); if ($this->confirm('Save failed items for retry?')) { $this->saveFailed($failed); } return Command::FAILURE; } $this->info('All items processed successfully!'); return Command::SUCCESS; } private function processItemWithRetry($item, $maxRetries) { for ($attempt = 1; $attempt <= $maxRetries; $attempt++) { try { $this->processItem($item); return true; } catch (\Exception $e) { $this->warn("Attempt {$attempt} failed for item {$item->id}: " . $e->getMessage()); if ($attempt < $maxRetries) { sleep(pow(2, $attempt)); // Exponential backoff } } } return false; }
16. Best Practices
1. Command Structure and Organization
class WellStructuredCommand extends Command { protected $signature = 'app:well-structured {--dry-run : Show what would be done without executing} {--verbose : Show detailed output} {--batch-size=100 : Batch size for processing}'; protected $description = 'A well-structured command example'; public function handle() { // Early validation if (!$this->validateInput()) { return Command::FAILURE; } // Setup $this->setupEnvironment(); // Main execution try { return $this->executeMain(); } catch (\Exception $e) { $this->handleError($e); return Command::FAILURE; } finally { $this->cleanup(); } } private function validateInput(): bool { // Input validation logic return true; } private function setupEnvironment(): void { // Environment setup } private function executeMain(): int { // Main logic return Command::SUCCESS; } private function handleError(\Exception $e): void { $this->error('Command failed: ' . $e->getMessage()); if ($this->option('verbose')) { $this->error($e->getTraceAsString()); } } private function cleanup(): void { // Cleanup logic } }
2. Testing Artisan Commands
// tests/Feature/ProcessDataCommandTest.php class ProcessDataCommandTest extends TestCase { /** @test */ public function it_processes_data_successfully() { // Arrange User::factory()->count(10)->create(); // Act $this->artisan('data:process') ->expectsQuestion('Do you want to continue?', 'yes') ->expectsOutput('Processing complete!') ->assertExitCode(0); } /** @test */ public function it_handles_user_cancellation() { $this->artisan('data:process') ->expectsQuestion('Do you want to continue?', 'no') ->expectsOutput('Operation cancelled.') ->assertExitCode(1); } /** @test */ public function it_respects_batch_size_option() { User::factory()->count(150)->create(); $this->artisan('data:process', ['--batch-size' => 50]) ->expectsQuestion('Do you want to continue?', 'yes') ->assertExitCode(0); } }
3. Logging and Monitoring
public function handle() { $startTime = microtime(true); $this->info('Starting process at ' . now()); Log::info('Command started', [ 'command' => $this->signature, 'options' => $this->options(), 'arguments' => $this->arguments(), ]); try { $result = $this->processData(); $duration = microtime(true) - $startTime; $this->info("Process completed in " . round($duration, 2) . " seconds"); Log::info('Command completed successfully', [ 'duration' => $duration, 'processed_count' => $result['processed'], ]); return Command::SUCCESS; } catch (\Exception $e) { Log::error('Command failed', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); throw $e; } }
4. Memory and Performance Optimization
public function handle() { // Monitor memory usage $this->info('Initial memory: ' . $this->formatBytes(memory_get_usage(true))); // Process in chunks to avoid memory issues $totalProcessed = 0; User::chunk(1000, function ($users) use (&$totalProcessed) { foreach ($users as $user) { $this->processUser($user); $totalProcessed++; // Memory cleanup every 100 items if ($totalProcessed % 100 === 0) { $this->line("Processed: {$totalProcessed}, Memory: " . $this->formatBytes(memory_get_usage(true))); // Force garbage collection gc_collect_cycles(); } } }); $this->info('Final memory: ' . $this->formatBytes(memory_get_usage(true))); } private function formatBytes($bytes, $precision = 2): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) { $bytes /= 1024; } return round($bytes, $precision) . ' ' . $units[$i]; }
17. Registering Commands
In app/Console/Kernel.php
protected $commands = [ \App\Console\Commands\ProcessDataCommand::class, ]; Or use automatic discovery in composer.json { "autoload": { "psr-4": { "App\\": "app/" } }, "extra": { "laravel": { "dont-discover": [] } } }
“Any sufficiently advanced technology is indistinguishable from magic.” — Arthur C. Clarke
18. Stats
Custom Artisan commands are widely used in Laravel projects
Developers frequently create custom commands for repetitive tasks such as database seeding, report generation, and background processing.
Source: Hostinger - Laravel Commands Tutorial
Typical Laravel projects include 5–15 custom Artisan commands
This range is based on examples from real-world projects and developer experience shared across blogs and tutorials.
Source: Medium - Streamlining Tasks with Custom Artisan Commands
Command execution performance has improved by 30–50% with Laravel 8+
Enhancements like route/view config caching and PHP 8 optimizations significantly reduced bootstrapping time.
Source: Honeybadger - Optimize Laravel Performance
19. Interesting Facts
- Artisan Name Origin: The name “Artisan” was chosen because it represents craftsmanship and skill — just like how developers craft custom commands to solve specific problems.
- Hidden Commands: Laravel has over 60 built-in Artisan commands, but only about 20 are commonly known and used by developers.
- Progress Bar Magic: Laravel’s progress bars can automatically estimate completion time using exponential smoothing algorithms based on processing speed.
- Memory Efficiency: Properly chunked Laravel commands can process millions of records using less than 50MB of memory.
- Background Processing: Laravel’s queue integration allows commands to process work 24/7 with automatic failure handling and retry logic.
20. FAQ’s
Q: Can I run multiple Artisan commands simultaneously?
A: Yes! You can run multiple commands in parallel using process pools, background jobs, or separate terminal sessions. However, be careful with database locks and shared resources.
Q: How do I test commands that use external APIs or services?
A: Use Laravel’s HTTP fake, mock external services, or create test doubles. The Artisan::call() method in tests allows you to simulate user input.
Q: What’s the difference between call() and callSilent() when chaining commands?
A: call() displays output from the called command, while callSilent() suppresses it. Use callSilent() when you want to control output formatting yourself.
Q: Can I create commands that modify themselves or generate other commands?
A: Yes! You can use Laravel’s file system and Artisan’s make::command functionality programmatically to generate commands dynamically.
Q: How do I debug commands that only fail in production?
A: Add comprehensive logging, use — verbose flags, implement error reporting, and consider using remote debugging tools or log aggregation services.
“The science of today is the technology of tomorrow.” — Edward Teller
21. Conclusion
Building advanced Artisan commands is a powerful way to enhance your Laravel applications with robust, interactive CLI tools. This comprehensive guide has covered the essential patterns and best practices for creating professional-grade commands that can handle complex data processing, user interaction, and background operations.
About the Author: Avinash is a web developer since 2008. Currently working at AddWebSolution, where he’s passionate about clean code, modern technologies, and building tools that make the web better.
Top comments (0)