DEV Community

Cover image for Building Custom Artisan Commands with Advanced Features

Building Custom Artisan Commands with Advanced Features

“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

  1. Basic Command Structure
  2. Basic Command Template
  3. Progress Bars
  4. Advanced Progress Bar with Custom Format
  5. Multiple Progress Bars
  6. Interactive Prompts
  7. Choice Selection
  8. Advanced Input Validation
  9. Interactive Menu System
  10. Background Processing
  11. Parallel Processing with Process Pools
  12. Long-Running Command with Signal Handling
  13. Advanced Features
  14. Configuration and Environment Detection
  15. Error Handling and Retry Logic
  16. Best Practices
  17. Registering Commands
  18. Stats
  19. Interesting Facts
  20. FAQ’s
  21. Conclusion

1. Basic Command Structure

Creating a New Command

php artisan make:command ProcessDataCommand 
Enter fullscreen mode Exit fullscreen mode

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; } } 
Enter fullscreen mode Exit fullscreen mode

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!'); } 
Enter fullscreen mode Exit fullscreen mode

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."); } 
Enter fullscreen mode Exit fullscreen mode

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(); } }); } 
Enter fullscreen mode Exit fullscreen mode

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}"); } 
Enter fullscreen mode Exit fullscreen mode

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)); } 
Enter fullscreen mode Exit fullscreen mode

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; } 
Enter fullscreen mode Exit fullscreen mode

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(); } 
Enter fullscreen mode Exit fullscreen mode

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); } } 
Enter fullscreen mode Exit fullscreen mode

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!'); } 
Enter fullscreen mode Exit fullscreen mode

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; } 
Enter fullscreen mode Exit fullscreen mode

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(); } 
Enter fullscreen mode Exit fullscreen mode

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); } 
Enter fullscreen mode Exit fullscreen mode

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; } 
Enter fullscreen mode Exit fullscreen mode

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 } } 
Enter fullscreen mode Exit fullscreen mode

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); } } 
Enter fullscreen mode Exit fullscreen mode

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; } } 
Enter fullscreen mode Exit fullscreen mode

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]; } 
Enter fullscreen mode Exit fullscreen mode

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": [] } } } 
Enter fullscreen mode Exit fullscreen mode

“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)