Navigation

Laravel

Creating Custom Laravel Artisan Commands: Advanced Techniques

Master advanced Laravel Artisan command techniques. Build powerful CLI tools with interactive workflows, external API integration, performance monitoring, and comprehensive testing strategies.

Master Laravel's Artisan command system to build powerful CLI tools that automate complex tasks, integrate with external systems, and enhance your development workflow.

Table Of Contents

Understanding Laravel's Artisan Architecture

Laravel's Artisan console provides a robust foundation for building command-line applications that integrate seamlessly with your Laravel ecosystem. Beyond simple commands, you can create sophisticated CLI tools that handle database operations, API integrations, file processing, and complex business logic workflows.

Creating custom Artisan commands becomes essential when building modular Laravel applications where different modules need specialized maintenance tasks, data migration utilities, and automated processes.

Advanced Command Structure and Organization

Base Command Architecture

Create a solid foundation for complex commands with proper structure and error handling:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

abstract class BaseCommand extends Command
{
    protected $signature = '';
    protected $description = '';
    
    protected bool $dryRun = false;
    protected bool $verbose = false;
    protected ProgressBar $progressBar;
    
    protected function configure()
    {
        parent::configure();
        
        $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Run command without making changes');
        $this->addOption('verbose', 'v', InputOption::VALUE_NONE, 'Enable verbose output');
        $this->addOption('chunk-size', null, InputOption::VALUE_OPTIONAL, 'Process records in chunks', 1000);
    }
    
    public function handle()
    {
        $this->dryRun = $this->option('dry-run');
        $this->verbose = $this->option('verbose');
        
        $this->info('Starting ' . $this->getName());
        $this->info('Dry run: ' . ($this->dryRun ? 'Yes' : 'No'));
        
        try {
            DB::beginTransaction();
            
            $result = $this->executeCommand();
            
            if ($this->dryRun) {
                DB::rollBack();
                $this->warn('Dry run completed - no changes were made');
            } else {
                DB::commit();
                $this->info('Command completed successfully');
            }
            
            return $result;
            
        } catch (\Exception $e) {
            DB::rollBack();
            $this->handleError($e);
            return Command::FAILURE;
        }
    }
    
    abstract protected function executeCommand(): int;
    
    protected function handleError(\Exception $e): void
    {
        $this->error('Command failed: ' . $e->getMessage());
        
        if ($this->verbose) {
            $this->error('Stack trace:');
            $this->error($e->getTraceAsString());
        }
        
        Log::error('Artisan command failed', [
            'command' => $this->getName(),
            'error' => $e->getMessage(),
            'trace' => $e->getTraceAsString(),
        ]);
    }
    
    protected function createProgressBar(int $max): ProgressBar
    {
        $this->progressBar = $this->output->createProgressBar($max);
        $this->progressBar->setFormat('verbose');
        return $this->progressBar;
    }
    
    protected function logProgress(string $message): void
    {
        if ($this->verbose) {
            $this->info($message);
        }
        
        Log::info($message, ['command' => $this->getName()]);
    }
}

Complex Data Processing Command

Build commands that handle large datasets efficiently:

<?php

namespace App\Console\Commands;

use App\Models\User;
use App\Services\EmailService;
use App\Jobs\ProcessUserDataJob;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;

class ProcessUserDataCommand extends BaseCommand
{
    protected $signature = 'users:process 
                           {--filter=* : Filter users by criteria}
                           {--action=email : Action to perform (email|export|cleanup)}
                           {--output= : Output file path for exports}
                           {--template= : Email template to use}
                           {--queue : Process in background queue}';
    
    protected $description = 'Process user data with various actions and filters';
    
    protected EmailService $emailService;
    protected array $processedUsers = [];
    protected array $failedUsers = [];
    
    public function __construct(EmailService $emailService)
    {
        parent::__construct();
        $this->emailService = $emailService;
    }
    
    protected function executeCommand(): int
    {
        $action = $this->option('action');
        $filters = $this->option('filter');
        $useQueue = $this->option('queue');
        
        $this->info("Processing users with action: {$action}");
        
        $query = $this->buildUserQuery($filters);
        $totalUsers = $query->count();
        
        if ($totalUsers === 0) {
            $this->warn('No users match the specified criteria');
            return Command::SUCCESS;
        }
        
        $this->info("Found {$totalUsers} users to process");
        
        if (!$this->confirm('Do you want to continue?')) {
            $this->info('Operation cancelled');
            return Command::SUCCESS;
        }
        
        $progressBar = $this->createProgressBar($totalUsers);
        $chunkSize = (int) $this->option('chunk-size');
        
        $query->chunk($chunkSize, function (Collection $users) use ($action, $useQueue, $progressBar) {
            foreach ($users as $user) {
                try {
                    if ($useQueue) {
                        ProcessUserDataJob::dispatch($user, $action, $this->getActionOptions());
                        $this->logProgress("Queued {$action} for user {$user->id}");
                    } else {
                        $this->processUser($user, $action);
                    }
                    
                    $this->processedUsers[] = $user->id;
                    
                } catch (\Exception $e) {
                    $this->failedUsers[] = [
                        'user_id' => $user->id,
                        'error' => $e->getMessage(),
                    ];
                    
                    $this->logProgress("Failed to process user {$user->id}: {$e->getMessage()}");
                }
                
                $progressBar->advance();
            }
        });
        
        $progressBar->finish();
        $this->newLine();
        
        $this->displayResults();
        
        return Command::SUCCESS;
    }
    
    protected function buildUserQuery(array $filters): Builder
    {
        $query = User::query();
        
        foreach ($filters as $filter) {
            [$field, $operator, $value] = $this->parseFilter($filter);
            
            switch ($field) {
                case 'role':
                    $query->whereHas('roles', fn($q) => $q->where('name', $value));
                    break;
                    
                case 'created_after':
                    $query->where('created_at', '>=', $value);
                    break;
                    
                case 'created_before':
                    $query->where('created_at', '<=', $value);
                    break;
                    
                case 'active':
                    $query->where('is_active', $value === 'true');
                    break;
                    
                case 'has_orders':
                    if ($value === 'true') {
                        $query->has('orders');
                    } else {
                        $query->doesntHave('orders');
                    }
                    break;
                    
                default:
                    $query->where($field, $operator, $value);
            }
            
            $this->logProgress("Applied filter: {$filter}");
        }
        
        return $query;
    }
    
    protected function parseFilter(string $filter): array
    {
        // Parse filters in format: field:operator:value or field:value
        $parts = explode(':', $filter);
        
        if (count($parts) === 2) {
            return [$parts[0], '=', $parts[1]];
        }
        
        if (count($parts) === 3) {
            return [$parts[0], $parts[1], $parts[2]];
        }
        
        throw new \InvalidArgumentException("Invalid filter format: {$filter}");
    }
    
    protected function processUser(User $user, string $action): void
    {
        switch ($action) {
            case 'email':
                $this->sendEmail($user);
                break;
                
            case 'export':
                $this->exportUser($user);
                break;
                
            case 'cleanup':
                $this->cleanupUser($user);
                break;
                
            default:
                throw new \InvalidArgumentException("Unknown action: {$action}");
        }
    }
    
    protected function sendEmail(User $user): void
    {
        $template = $this->option('template') ?? 'default';
        
        if (!$this->dryRun) {
            $this->emailService->sendToUser($user, $template);
        }
        
        $this->logProgress("Email sent to user {$user->id} using template {$template}");
    }
    
    protected function exportUser(User $user): void
    {
        $outputFile = $this->option('output') ?? storage_path('exports/users.csv');
        
        $userData = [
            $user->id,
            $user->email,
            $user->name,
            $user->created_at->toDateTimeString(),
            $user->orders_count ?? 0,
        ];
        
        if (!$this->dryRun) {
            file_put_contents($outputFile, implode(',', $userData) . PHP_EOL, FILE_APPEND | LOCK_EX);
        }
        
        $this->logProgress("Exported user {$user->id} to {$outputFile}");
    }
    
    protected function cleanupUser(User $user): void
    {
        // Remove old sessions, temporary data, etc.
        if (!$this->dryRun) {
            $user->sessions()->where('last_activity', '<', now()->subDays(30))->delete();
            $user->temporaryFiles()->where('created_at', '<', now()->subDays(7))->delete();
        }
        
        $this->logProgress("Cleaned up data for user {$user->id}");
    }
    
    protected function getActionOptions(): array
    {
        return [
            'template' => $this->option('template'),
            'output' => $this->option('output'),
            'dry_run' => $this->dryRun,
        ];
    }
    
    protected function displayResults(): void
    {
        $this->table(
            ['Metric', 'Count'],
            [
                ['Processed Users', count($this->processedUsers)],
                ['Failed Users', count($this->failedUsers)],
            ]
        );
        
        if (!empty($this->failedUsers)) {
            $this->error('Failed users:');
            $this->table(
                ['User ID', 'Error'],
                $this->failedUsers
            );
        }
    }
}

Interactive Commands and User Input

Complex Interactive Command

Create commands that guide users through complex workflows:

<?php

namespace App\Console\Commands;

use App\Models\User;
use App\Models\Role;
use App\Services\DeploymentService;
use Illuminate\Support\Facades\Validator;

class SystemMaintenanceCommand extends BaseCommand
{
    protected $signature = 'system:maintenance';
    protected $description = 'Interactive system maintenance wizard';
    
    protected DeploymentService $deploymentService;
    protected array $maintenanceTasks = [];
    
    public function __construct(DeploymentService $deploymentService)
    {
        parent::__construct();
        $this->deploymentService = $deploymentService;
    }
    
    protected function executeCommand(): int
    {
        $this->displayWelcome();
        
        $mode = $this->choice(
            'Select maintenance mode:',
            ['scheduled', 'emergency', 'routine'],
            'routine'
        );
        
        $this->info("Selected mode: {$mode}");
        
        switch ($mode) {
            case 'scheduled':
                return $this->handleScheduledMaintenance();
                
            case 'emergency':
                return $this->handleEmergencyMaintenance();
                
            case 'routine':
                return $this->handleRoutineMaintenance();
        }
        
        return Command::SUCCESS;
    }
    
    protected function displayWelcome(): void
    {
        $this->info('╔══════════════════════════════════════╗');
        $this->info('║      System Maintenance Wizard      ║');
        $this->info('╚══════════════════════════════════════╝');
        $this->newLine();
    }
    
    protected function handleScheduledMaintenance(): int
    {
        $this->info('🕒 Scheduled Maintenance Mode');
        
        $startTime = $this->askForDateTime('Enter maintenance start time');
        $duration = $this->askForDuration('Enter estimated duration (minutes)');
        
        $tasks = $this->selectMaintenanceTasks();
        
        $this->displayMaintenancePlan($startTime, $duration, $tasks);
        
        if (!$this->confirm('Proceed with scheduled maintenance?')) {
            return Command::SUCCESS;
        }
        
        return $this->executeMaintenanceTasks($tasks);
    }
    
    protected function handleEmergencyMaintenance(): int
    {
        $this->error('🚨 Emergency Maintenance Mode');
        
        $issue = $this->ask('Describe the emergency issue');
        $severity = $this->choice('Severity level:', ['low', 'medium', 'high', 'critical'], 'high');
        
        $this->warn("Issue: {$issue}");
        $this->warn("Severity: {$severity}");
        
        if ($severity === 'critical') {
            $this->error('Critical issue detected - initiating immediate maintenance');
            return $this->executeCriticalMaintenance($issue);
        }
        
        $tasks = $this->recommendEmergencyTasks($issue, $severity);
        
        return $this->executeMaintenanceTasks($tasks);
    }
    
    protected function handleRoutineMaintenance(): int
    {
        $this->info('🔧 Routine Maintenance Mode');
        
        $tasks = $this->selectFromRoutineTasks();
        
        if (empty($tasks)) {
            $this->info('No tasks selected');
            return Command::SUCCESS;
        }
        
        return $this->executeMaintenanceTasks($tasks);
    }
    
    protected function selectMaintenanceTasks(): array
    {
        $availableTasks = [
            'database_optimize' => 'Optimize database tables',
            'cache_clear' => 'Clear application cache',
            'log_cleanup' => 'Clean up old log files',
            'backup_create' => 'Create system backup',
            'security_scan' => 'Run security scan',
            'performance_check' => 'Performance health check',
            'dependency_update' => 'Update dependencies',
            'ssl_renewal' => 'Renew SSL certificates',
        ];
        
        $selectedTasks = [];
        
        while (true) {
            $this->table(['Task', 'Description'], 
                collect($availableTasks)->map(fn($desc, $task) => [$task, $desc])->toArray()
            );
            
            $task = $this->choice(
                'Select a task (or "done" to finish):',
                array_merge(array_keys($availableTasks), ['done']),
                'done'
            );
            
            if ($task === 'done') {
                break;
            }
            
            if (!in_array($task, $selectedTasks)) {
                $selectedTasks[] = $task;
                $this->info("✓ Added: {$availableTasks[$task]}");
            } else {
                $this->warn("Task already selected: {$task}");
            }
        }
        
        return $selectedTasks;
    }
    
    protected function askForDateTime(string $question): \DateTime
    {
        while (true) {
            $input = $this->ask($question . ' (Y-m-d H:i format)');
            
            try {
                return new \DateTime($input);
            } catch (\Exception $e) {
                $this->error('Invalid date/time format. Please use Y-m-d H:i (e.g., 2024-01-15 02:00)');
            }
        }
    }
    
    protected function askForDuration(string $question): int
    {
        while (true) {
            $input = $this->ask($question);
            
            if (is_numeric($input) && $input > 0) {
                return (int) $input;
            }
            
            $this->error('Please enter a valid positive number');
        }
    }
    
    protected function displayMaintenancePlan(\DateTime $startTime, int $duration, array $tasks): void
    {
        $this->info('📋 Maintenance Plan:');
        $this->table(
            ['Detail', 'Value'],
            [
                ['Start Time', $startTime->format('Y-m-d H:i:s')],
                ['Duration', $duration . ' minutes'],
                ['End Time', $startTime->modify("+{$duration} minutes")->format('Y-m-d H:i:s')],
                ['Tasks', implode(', ', $tasks)],
            ]
        );
    }
    
    protected function executeMaintenanceTasks(array $tasks): int
    {
        $progressBar = $this->createProgressBar(count($tasks));
        $results = [];
        
        foreach ($tasks as $task) {
            $progressBar->setMessage("Executing: {$task}");
            
            try {
                $result = $this->executeTask($task);
                $results[$task] = ['status' => 'success', 'message' => $result];
                
            } catch (\Exception $e) {
                $results[$task] = ['status' => 'failed', 'message' => $e->getMessage()];
                $this->error("Task failed: {$task} - {$e->getMessage()}");
            }
            
            $progressBar->advance();
        }
        
        $progressBar->finish();
        $this->newLine();
        
        $this->displayTaskResults($results);
        
        return Command::SUCCESS;
    }
    
    protected function executeTask(string $task): string
    {
        if ($this->dryRun) {
            return "Dry run: {$task} would be executed";
        }
        
        switch ($task) {
            case 'database_optimize':
                return $this->optimizeDatabase();
                
            case 'cache_clear':
                \Artisan::call('cache:clear');
                \Artisan::call('config:clear');
                \Artisan::call('route:clear');
                \Artisan::call('view:clear');
                return 'All caches cleared successfully';
                
            case 'log_cleanup':
                return $this->cleanupLogs();
                
            case 'backup_create':
                return $this->createBackup();
                
            case 'security_scan':
                return $this->runSecurityScan();
                
            case 'performance_check':
                return $this->performanceCheck();
                
            default:
                throw new \InvalidArgumentException("Unknown task: {$task}");
        }
    }
    
    protected function optimizeDatabase(): string
    {
        \DB::statement('OPTIMIZE TABLE users, orders, products');
        return 'Database tables optimized';
    }
    
    protected function cleanupLogs(): string
    {
        $logPath = storage_path('logs');
        $files = glob($logPath . '/*.log');
        $deletedCount = 0;
        
        foreach ($files as $file) {
            if (filemtime($file) < strtotime('-30 days')) {
                unlink($file);
                $deletedCount++;
            }
        }
        
        return "Deleted {$deletedCount} old log files";
    }
    
    protected function createBackup(): string
    {
        // Implementation would depend on your backup strategy
        $backupName = 'maintenance_backup_' . date('Y_m_d_H_i_s');
        return "Backup created: {$backupName}";
    }
    
    protected function runSecurityScan(): string
    {
        // Run security checks
        return 'Security scan completed - no issues found';
    }
    
    protected function performanceCheck(): string
    {
        $memoryUsage = memory_get_usage(true);
        $diskSpace = disk_free_space('/');
        
        return sprintf(
            'Memory: %s MB, Free disk: %s GB',
            round($memoryUsage / 1024 / 1024, 2),
            round($diskSpace / 1024 / 1024 / 1024, 2)
        );
    }
    
    protected function displayTaskResults(array $results): void
    {
        $this->info('📊 Task Results:');
        
        $tableData = [];
        foreach ($results as $task => $result) {
            $status = $result['status'] === 'success' ? '✅' : '❌';
            $tableData[] = [$status, $task, $result['message']];
        }
        
        $this->table(['Status', 'Task', 'Result'], $tableData);
    }
}

Integration with External Systems

API Integration Command

Create commands that interact with external APIs and services:

<?php

namespace App\Console\Commands;

use App\Services\ExternalApiService;
use App\Services\DataSyncService;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Response;

class SyncExternalDataCommand extends BaseCommand
{
    protected $signature = 'sync:external-data 
                           {service : External service name}
                           {--endpoint= : Specific endpoint to sync}
                           {--since= : Sync data since date}
                           {--limit=1000 : Limit number of records}
                           {--retry=3 : Number of retry attempts}';
    
    protected $description = 'Sync data from external APIs';
    
    protected ExternalApiService $apiService;
    protected DataSyncService $syncService;
    
    public function __construct(
        ExternalApiService $apiService,
        DataSyncService $syncService
    ) {
        parent::__construct();
        $this->apiService = $apiService;
        $this->syncService = $syncService;
    }
    
    protected function executeCommand(): int
    {
        $service = $this->argument('service');
        $endpoint = $this->option('endpoint');
        $since = $this->option('since');
        $limit = (int) $this->option('limit');
        $retryAttempts = (int) $this->option('retry');
        
        $this->info("Starting sync for service: {$service}");
        
        $endpoints = $endpoint ? [$endpoint] : $this->getAvailableEndpoints($service);
        
        foreach ($endpoints as $ep) {
            $this->info("Syncing endpoint: {$ep}");
            
            try {
                $this->syncEndpoint($service, $ep, $since, $limit, $retryAttempts);
                $this->info("✅ Successfully synced {$ep}");
                
            } catch (\Exception $e) {
                $this->error("❌ Failed to sync {$ep}: {$e->getMessage()}");
            }
        }
        
        return Command::SUCCESS;
    }
    
    protected function syncEndpoint(
        string $service, 
        string $endpoint, 
        ?string $since, 
        int $limit, 
        int $retryAttempts
    ): void {
        $page = 1;
        $totalSynced = 0;
        
        do {
            $params = [
                'page' => $page,
                'limit' => min($limit - $totalSynced, 100), // Max 100 per request
            ];
            
            if ($since) {
                $params['since'] = $since;
            }
            
            $response = $this->makeApiRequest($service, $endpoint, $params, $retryAttempts);
            $data = $response->json();
            
            if (empty($data['data'])) {
                break;
            }
            
            $syncResult = $this->syncService->syncData($service, $endpoint, $data['data']);
            
            $totalSynced += count($data['data']);
            $this->logProgress("Synced page {$page}: {$syncResult['created']} created, {$syncResult['updated']} updated");
            
            $page++;
            
        } while (
            !empty($data['data']) && 
            $totalSynced < $limit && 
            ($data['has_more'] ?? false)
        );
        
        $this->info("Total records synced for {$endpoint}: {$totalSynced}");
    }
    
    protected function makeApiRequest(
        string $service, 
        string $endpoint, 
        array $params, 
        int $retryAttempts
    ): Response {
        $attempt = 1;
        
        while ($attempt <= $retryAttempts) {
            try {
                $response = $this->apiService->request($service, $endpoint, $params);
                
                if ($response->successful()) {
                    return $response;
                }
                
                if ($response->status() === 429) {
                    $retryAfter = $response->header('Retry-After', 60);
                    $this->warn("Rate limited. Waiting {$retryAfter} seconds...");
                    sleep((int) $retryAfter);
                    $attempt++;
                    continue;
                }
                
                throw new \Exception("API request failed: {$response->status()} - {$response->body()}");
                
            } catch (\Exception $e) {
                if ($attempt === $retryAttempts) {
                    throw $e;
                }
                
                $this->warn("Attempt {$attempt} failed: {$e->getMessage()}. Retrying...");
                sleep(pow(2, $attempt)); // Exponential backoff
                $attempt++;
            }
        }
        
        throw new \Exception('Max retry attempts exceeded');
    }
    
    protected function getAvailableEndpoints(string $service): array
    {
        $endpoints = [
            'shopify' => ['products', 'orders', 'customers'],
            'salesforce' => ['leads', 'accounts', 'opportunities'],
            'stripe' => ['customers', 'charges', 'subscriptions'],
        ];
        
        return $endpoints[$service] ?? throw new \InvalidArgumentException("Unknown service: {$service}");
    }
}

Testing Custom Commands

Command Testing Strategies

Create comprehensive tests for your custom commands:

<?php

namespace Tests\Feature\Console;

use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Mail;

class ProcessUserDataCommandTest extends TestCase
{
    use RefreshDatabase;
    
    public function test_processes_users_with_email_action(): void
    {
        Mail::fake();
        
        $users = User::factory()->count(5)->create();
        
        $this->artisan('users:process', [
            '--action' => 'email',
            '--template' => 'welcome',
        ])
        ->expectsQuestion('Do you want to continue?', 'yes')
        ->expectsOutput('Found 5 users to process')
        ->expectsOutput('Command completed successfully')
        ->assertExitCode(0);
        
        Mail::assertSent(\App\Mail\WelcomeEmail::class, 5);
    }
    
    public function test_dry_run_does_not_send_emails(): void
    {
        Mail::fake();
        
        User::factory()->count(3)->create();
        
        $this->artisan('users:process', [
            '--action' => 'email',
            '--dry-run' => true,
        ])
        ->expectsOutput('Dry run completed - no changes were made')
        ->assertExitCode(0);
        
        Mail::assertNothingSent();
    }
    
    public function test_filters_users_correctly(): void
    {
        $activeUser = User::factory()->create(['is_active' => true]);
        $inactiveUser = User::factory()->create(['is_active' => false]);
        
        $this->artisan('users:process', [
            '--action' => 'export',
            '--filter' => ['active:true'],
            '--output' => storage_path('test_export.csv'),
        ])
        ->expectsQuestion('Do you want to continue?', 'yes')
        ->expectsOutput('Found 1 users to process')
        ->assertExitCode(0);
        
        $this->assertFileExists(storage_path('test_export.csv'));
        $content = file_get_contents(storage_path('test_export.csv'));
        $this->assertStringContains($activeUser->email, $content);
        $this->assertStringNotContains($inactiveUser->email, $content);
    }
    
    public function test_handles_queue_option(): void
    {
        Queue::fake();
        
        User::factory()->count(2)->create();
        
        $this->artisan('users:process', [
            '--action' => 'email',
            '--queue' => true,
        ])
        ->expectsQuestion('Do you want to continue?', 'yes')
        ->assertExitCode(0);
        
        Queue::assertPushed(\App\Jobs\ProcessUserDataJob::class, 2);
    }
    
    public function test_handles_invalid_filter_format(): void
    {
        $this->artisan('users:process', [
            '--filter' => ['invalid-filter-format'],
        ])
        ->expectsOutput('Command failed: Invalid filter format: invalid-filter-format')
        ->assertExitCode(1);
    }
    
    public function test_maintenance_command_scheduled_mode(): void
    {
        $this->artisan('system:maintenance')
            ->expectsChoice('Select maintenance mode:', 'scheduled')
            ->expectsQuestion('Enter maintenance start time (Y-m-d H:i format)', '2024-01-15 02:00')
            ->expectsQuestion('Enter estimated duration (minutes)', '60')
            ->expectsChoice('Select a task (or "done" to finish):', 'cache_clear')
            ->expectsChoice('Select a task (or "done" to finish):', 'done')
            ->expectsQuestion('Proceed with scheduled maintenance?', 'yes')
            ->expectsOutput('All caches cleared successfully')
            ->assertExitCode(0);
    }
    
    protected function tearDown(): void
    {
        // Clean up test files
        $testFiles = [
            storage_path('test_export.csv'),
        ];
        
        foreach ($testFiles as $file) {
            if (file_exists($file)) {
                unlink($file);
            }
        }
        
        parent::tearDown();
    }
}

Performance and Monitoring

Command Performance Monitoring

Add performance monitoring to your commands:

<?php

namespace App\Console\Commands\Concerns;

use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Log;

trait MonitorsPerformance
{
    protected float $startTime;
    protected array $metrics = [];
    protected string $commandKey;
    
    protected function startMonitoring(): void
    {
        $this->startTime = microtime(true);
        $this->commandKey = 'command:' . $this->getName() . ':' . date('Y-m-d-H-i-s');
        
        $this->metrics = [
            'start_time' => $this->startTime,
            'memory_start' => memory_get_usage(true),
            'peak_memory' => 0,
            'processed_items' => 0,
            'errors' => 0,
        ];
        
        Redis::hset($this->commandKey, $this->metrics);
        Redis::expire($this->commandKey, 86400); // 24 hours
    }
    
    protected function recordProgress(int $itemsProcessed = 1): void
    {
        $this->metrics['processed_items'] += $itemsProcessed;
        $this->metrics['peak_memory'] = max(
            $this->metrics['peak_memory'], 
            memory_get_usage(true)
        );
        
        Redis::hset($this->commandKey, [
            'processed_items' => $this->metrics['processed_items'],
            'peak_memory' => $this->metrics['peak_memory'],
            'current_memory' => memory_get_usage(true),
            'elapsed_time' => microtime(true) - $this->startTime,
        ]);
    }
    
    protected function recordError(\Exception $e): void
    {
        $this->metrics['errors']++;
        
        Redis::hset($this->commandKey, [
            'errors' => $this->metrics['errors'],
            'last_error' => $e->getMessage(),
        ]);
        
        Log::error('Command error', [
            'command' => $this->getName(),
            'error' => $e->getMessage(),
            'metrics' => $this->metrics,
        ]);
    }
    
    protected function finishMonitoring(): void
    {
        $endTime = microtime(true);
        $totalTime = $endTime - $this->startTime;
        
        $finalMetrics = [
            'end_time' => $endTime,
            'total_time' => $totalTime,
            'items_per_second' => $this->metrics['processed_items'] / $totalTime,
            'memory_efficiency' => $this->metrics['peak_memory'] / 1024 / 1024, // MB
            'status' => 'completed',
        ];
        
        Redis::hset($this->commandKey, $finalMetrics);
        
        $this->displayPerformanceReport($finalMetrics);
        
        Log::info('Command completed', [
            'command' => $this->getName(),
            'metrics' => array_merge($this->metrics, $finalMetrics),
        ]);
    }
    
    protected function displayPerformanceReport(array $finalMetrics): void
    {
        $this->newLine();
        $this->info('📊 Performance Report:');
        $this->table(
            ['Metric', 'Value'],
            [
                ['Total Time', number_format($finalMetrics['total_time'], 2) . 's'],
                ['Items Processed', number_format($this->metrics['processed_items'])],
                ['Items/Second', number_format($finalMetrics['items_per_second'], 2)],
                ['Peak Memory', number_format($finalMetrics['memory_efficiency'], 2) . ' MB'],
                ['Errors', $this->metrics['errors']],
            ]
        );
    }
}

Creating advanced Artisan commands transforms Laravel applications into powerful CLI-driven systems. These techniques enable building sophisticated automation tools that integrate seamlessly with your Laravel service architecture while providing robust error handling, monitoring, and user interaction capabilities.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Laravel