The Laravel Artisan Console and Custom Commands
Artisan is Laravel's "Swiss Army knife"—with over 100 built-in commands covering all operations and maintenance tasks, and custom commands that let you handle repetitive tasks with a single click.
1. What You'll Learn
- Complete list of built-in commands: migrate/cache/config/route/list
- Creating Custom Commands:
make:commandand Signature Syntax - Command Arguments and Options: {argument} / {--option} Definition
- Command Scheduling: app/Console/Kernel.php Cron Tasks
- Interactive commands: confirm/choice/ask/anticipate
2. A True Story of an Operations Engineer
(1) Pain Point: Manually performing 20 operations tasks every day
Every morning, Charlie has to manually perform the following tasks: clear expired sessions, generate daily reports, sync subscription statuses, send expiration reminder emails, back up the database... Twenty tasks spread across five terminal windows—if he misses even one, he'll have to pay out of pocket. Bob asked, "Aren't these all scheduled tasks? Why do you have to do them manually?"
(2) Solutions for Artisan Commands and Scheduling
Artisan custom commands wrap repetitive tasks into a single command, and Schedule automatically runs them on a schedule—Charlie only needs to glance at the execution log once a day.
# One command does everything
php artisan shopmetrics:daily-maintenance
# Or let the scheduler run it automatically at 2 AM
php artisan schedule:run
(3) Revenue
After Charlie set up scheduling with Artisan, all 20 tasks ran automatically, and the failure rate dropped from 5% to 0%.
3. Complete List of Built-in Commands
(1) Categories of Common Commands
| Category | Command | Description |
|---|---|---|
| Application | about |
Environmental Information Overview |
down / up |
Maintenance Mode Switch | |
env |
Show .env configuration | |
| Database | migrate |
Execute Migration |
migrate:rollback |
Rollback Migration | |
migrate:fresh |
Rebuild Database | |
db:seed |
Data Filling | |
db:show |
Database Information | |
| Cache | cache:clear |
Clear Cache |
config:cache / clear |
Configuration Cache | |
route:cache / clear |
Routing Cache | |
view:cache / clear |
View Cache | |
| Routes | route:list |
List all routes |
| Queue | queue:work |
Start Worker |
queue:failed |
View Failed Tasks | |
queue:retry |
Tasks that failed on retry | |
| Generate | make:model |
Create Model |
make:controller |
Create Controller | |
make:migration |
Create Migration | |
make:command |
Create Command |
(1) ▶ Example: Quick Reference for Common Artisan Commands
# App info
php artisan about
php artisan env
# Database operations
php artisan migrate --seed
php artisan db:show --counts
# Cache management
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize # Cache config + route + view
# Maintenance mode
php artisan down --secret="maintenance-token" # Allow access with ?secret=
php artisan up
# Queue management
php artisan queue:work --queue=high,default
php artisan queue:failed
php artisan queue:retry all
# Route inspection
php artisan route:list --path=api
php artisan route:list --columns=method,uri,name
Output:
# Command executed successfully
4. Custom Commands
(1) Create Command
php artisan make:command ProcessTenantAnalytics
# Creates: app/Console/Commands/ProcessTenantAnalytics.php
(2) Command Signature Syntax
// app/Console/Commands/ProcessTenantAnalytics.php
class ProcessTenantAnalytics extends Command
{
// Signature syntax: {argument} {--option}
protected $signature = 'shopmetrics:analytics
{tenant? : Tenant ID or slug (optional)}
{--type=monthly : Report type (daily|weekly|monthly)}
{--force : Force re-calculation}
{--format=csv : Output format (csv|json)}';
protected $description = 'Process analytics for tenants';
public function handle(): int
{
$tenant = $this->argument('tenant');
$type = $this->option('type');
$force = $this->option('force');
if ($tenant) {
$this->processSingleTenant($tenant, $type, $force);
} else {
$this->processAllTenants($type, $force);
}
return self::SUCCESS;
}
}
(3) Signature Syntax Rules
| Syntax | Description | Example |
|---|---|---|
{name} |
Required parameter | {tenant} |
{name?} |
Optional parameters | {tenant?} |
{name=default} |
With default value | {type=monthly} |
{--option} |
Boolean Options | --force |
{--option=default} |
Options with Values | --format=csv |
{--O|shortcut} |
Short options | --force|f |
(1) ▶ Example: ShopMetrics Tenant Management Commands
// app/Console/Commands/ManageTenant.php
class ManageTenant extends Command
{
protected $signature = 'shopmetrics:tenant
{action : Action to perform (list|suspend|activate|stats)}
{tenant? : Tenant ID or slug}
{--with-users : Include user statistics}';
protected $description = 'Manage ShopMetrics tenants';
public function handle(): int
{
$action = $this->argument('action');
match ($action) {
'list' => $this->listTenants(),
'suspend' => $this->suspendTenant(),
'activate' => $this->activateTenant(),
'stats' => $this->showTenantStats(),
default => $this->error("Unknown action: {$action}"),
};
return self::SUCCESS;
}
private function listTenants(): void
{
$tenants = Tenant::withCount(['shops', 'users'])->get();
$this->table(
['ID', 'Name', 'Slug', 'Status', 'Shops', 'Users'],
$tenants->map(fn ($t) => [
$t->id, $t->name, $t->slug, $t->status,
$t->shops_count, $t->users_count,
])
);
}
private function suspendTenant(): void
{
$identifier = $this->argument('tenant') ?? $this->ask('Enter tenant ID or slug:');
$tenant = $this->resolveTenant($identifier);
$tenant->update(['status' => 'suspended']);
$this->info("Tenant {$tenant->name} has been suspended.");
}
private function resolveTenant(string $identifier): Tenant
{
return is_numeric($identifier)
? Tenant::findOrFail($identifier)
: Tenant::whereSlug($identifier)->firstOrFail();
}
}
Output:
// Execution Successful
5. Command Arguments and Options
(1) Retrieve Parameters
// Arguments
$name = $this->argument('name'); // Single argument
$all = $this->arguments(); // All arguments as array
// Options
$force = $this->option('force'); // Single option (boolean or value)
$all = $this->options(); // All options as array
(2) Array Parameters
// Signature with array argument
protected $signature = 'shopmetrics:report
{tenants* : One or more tenant IDs}
{--type=monthly}';
// Usage
php artisan shopmetrics:report 1 2 3 --type=weekly
// Access
$tenants = $this->argument('tenants'); // [1, 2, 3]
(3) Output Methods
| Method | Description | Sample Output |
|---|---|---|
info() |
Green Information | ✓ Done |
error() |
Red Error | ✗ Failed |
warn() |
Yellow Warning | ⚠ Warning |
line() |
Plain text | Plain text |
table() |
Table | Format Table |
progressBar() |
Progress Bar | ██████░░ 60% |
(1) ▶ Example: ShopMetrics data cleaning command with a progress bar
// app/Console/Commands/CleanupExpiredData.php
class CleanupExpiredData extends Command
{
protected $signature = 'shopmetrics:cleanup
{--days=90 : Delete data older than N days}
{--dry-run : Show what would be deleted}';
protected $description = 'Clean up expired data (old reports, expired tokens)';
public function handle(): int
{
$days = $this->option('days');
$dryRun = $this->option('dry-run');
$cutoff = now()->subDays($days);
// Clean expired tokens
$expiredTokens = Sanctum::$personalAccessTokenModel::where('last_used_at', '<', $cutoff);
$this->info(($dryRun ? 'Would delete' : 'Deleting') . " {$expiredTokens->count()} expired tokens.");
// Clean old reports from S3
$bar = $this->output->createProgressBar(Tenant::count());
$deletedFiles = 0;
Tenant::chunk(100, function ($tenants) use ($cutoff, $dryRun, &$deletedFiles, $bar) {
foreach ($tenants as $tenant) {
$files = Storage::disk('s3')->allFiles("reports/{$tenant->slug}");
foreach ($files as $file) {
if (Storage::disk('s3')->lastModified($file) < $cutoff->timestamp) {
if (!$dryRun) Storage::disk('s3')->delete($file);
$deletedFiles++;
}
}
$bar->advance();
}
});
$bar->finish();
$this->newLine();
$this->info(($dryRun ? 'Would delete' : 'Deleted') . " {$deletedFiles} old report files.");
if (!$dryRun) {
$expiredTokens->delete();
}
return self::SUCCESS;
}
}
Output:
// Execution Successful
6. Command Scheduling
(1) Definition of Scheduling
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('shopmetrics:analytics --type=daily')
->dailyAt('02:00')
->onOneServer()
->withoutOverlapping()
->emailOutputOnFailure('admin@shopmetrics.io');
Schedule::command('shopmetrics:cleanup --days=90')
->weekly()
->sundays()
->at('03:00')
->onOneServer();
Schedule::command('shopmetrics:sync-subscriptions')
->dailyAt('06:00')
->onOneServer()
->withoutOverlapping();
Schedule::job(new ProcessMonthlyAnalyticsJob)
->monthlyOn(1, '00:00')
->onOneServer();
(2) Scheduling Frequency Options
| Method | Frequency | Equivalent Cron |
|---|---|---|
everyMinute() |
Per Minute | * * * * * |
everyFiveMinutes() |
Every 5 minutes | */5 * * * * |
hourly() |
Per hour | 0 * * * * |
daily() |
12:00 a.m. every day | 0 0 * * * |
dailyAt('14:00') |
2:00 p.m. daily | 0 14 * * * |
weekly() |
Every Sunday at midnight | 0 0 * * 0 |
monthly() |
12:00 a.m. on the 1st of each month | 0 0 1 * * |
cron('...') |
Custom Cron | Any |
(3) Scheduling Constraints
| Method | Description |
|---|---|
onOneServer() |
Run on only one server |
withoutOverlapping() |
Overlapping execution is not allowed |
runInBackground() |
Run in the background |
when(Closure) |
Conditional Execution |
environments('prod') |
Specified Environment |
emailOutputOnFailure() |
Email notification in case of failure |
(1) ▶ Example: Complete ShopMetrics Scheduling Configuration
// routes/console.php
Schedule::command('shopmetrics:analytics --type=daily')
->dailyAt('02:00')
->onOneServer()
->withoutOverlapping(60)
->emailOutputOnFailure('ops@shopmetrics.io');
Schedule::command('shopmetrics:cleanup --days=90')
->weeklyOn(Schedule::SUNDAY, '03:00')
->onOneServer();
Schedule::command('shopmetrics:send-expiring-notifications')
->dailyAt('08:00')
->when(fn () => Subscription::expiringSoon()->exists());
Schedule::command('queue:prune-failed --hours=168')
->daily();
Schedule::command('queue:prune-batches --hours=168')
->daily();
// Cron entry on server
// * * * * * cd /var/www/shopmetrics && php artisan schedule:run >> /dev/null 2>&1
Output:
// Execution Successful
7. Interactive Commands
(1) Interaction Methods
// Ask for input
$name = $this->ask('What is the tenant name?');
// Ask with default
$email = $this->ask('Email address?', 'admin@example.com');
// Secret input (passwords)
$password = $this->secret('Enter password:');
// Confirm (yes/no)
if ($this->confirm('Do you wish to continue?', true)) {
// Default: yes
}
// Choice (single select)
$type = $this->choice(
'Select report type',
['daily', 'weekly', 'monthly'],
0 // default index
);
// Anticipate (autocomplete)
$name = $this->anticipate('Tenant name', Tenant::pluck('name')->toArray());
(2) Comprehensive Interaction Workflow
// app/Console/Commands/SetupTenant.php
class SetupTenant extends Command
{
protected $signature = 'shopmetrics:tenant-setup';
protected $description = 'Interactive tenant setup wizard';
public function handle(): int
{
$this->info('=== ShopMetrics Tenant Setup Wizard ===');
$name = $this->ask('Tenant name');
$slug = $this->anticipate('Slug', [Str::slug($name)]);
$domain = $this->ask('Custom domain (optional)', $slug . '.shopmetrics.io');
$plan = $this->choice('Select plan', ['Starter', 'Pro', 'Enterprise'], 1);
$ownerEmail = $this->ask('Owner email');
$this->table(
['Field', 'Value'],
[['Name', $name], ['Slug', $slug], ['Domain', $domain], ['Plan', $plan], ['Owner', $ownerEmail]],
);
if (!$this->confirm('Create this tenant?', true)) {
$this->warn('Cancelled.');
return self::FAILURE;
}
$tenant = Tenant::create(compact('name', 'slug', 'domain'));
User::factory()->create([
'tenant_id' => $tenant->id,
'email' => $ownerEmail,
'role' => 'tenant_owner',
]);
$this->info("Tenant {$name} created successfully!");
return self::SUCCESS;
}
}
(1) ▶ Example: ShopMetrics Interactive Data Export Command
// app/Console/Commands/ExportData.php
class ExportData extends Command
{
protected $signature = 'shopmetrics:export';
protected $description = 'Interactive data export tool';
public function handle(): int
{
$type = $this->choice('What to export?', [
'orders' => 'Orders',
'products' => 'Products',
'analytics' => 'Analytics Report',
]);
$tenant = $this->anticipate('Tenant (leave blank for all)', Tenant::pluck('name')->push('All')->toArray());
$format = $this->choice('Format?', ['csv', 'xlsx', 'json'], 0);
$dateFrom = $this->ask('Date from (Y-m-d, optional)');
$dateTo = $this->ask('Date to (Y-m-d, optional)');
$this->info("Exporting {$type} for {$tenant} in {$format} format...");
$query = match ($type) {
'orders' => Order::query(),
'products' => Product::query(),
'analytics' => AnalyticsReport::query(),
};
if ($tenant !== 'All') {
$tenantModel = Tenant::whereName($tenant)->firstOrFail();
$query->where('tenant_id', $tenantModel->id);
}
if ($dateFrom) $query->where('created_at', '>=', $dateFrom);
if ($dateTo) $query->where('created_at', '<=', $dateTo);
$count = $query->count();
$this->info("Found {$count} records.");
if (!$this->confirm("Export {$count} records?", true)) {
return self::FAILURE;
}
$path = ExportService::export($query, $format);
$this->info("Export saved to: {$path}");
return self::SUCCESS;
}
}
Output:
// Execution Successful
8. Comprehensive Example: ShopMetrics Operations Command Set
// ============================================
// Comprehensive: ShopMetrics Operations Commands
// Covers: signature, options, progress, scheduling, interactive
// ============================================
// app/Console/Commands/DailyMaintenance.php
class DailyMaintenance extends Command
{
protected $signature = 'shopmetrics:daily-maintenance
{--skip-analytics : Skip analytics processing}
{--skip-cleanup : Skip data cleanup}
{--notify : Send completion notification}';
protected $description = 'Run daily maintenance tasks';
public function handle(): int
{
$this->info('Starting daily maintenance...');
if (!$this->option('skip-analytics')) {
$this->processAnalytics();
}
if (!$this->option('skip-cleanup')) {
$this->cleanupExpiredData();
}
$this->syncSubscriptions();
if ($this->option('notify')) {
$this->sendCompletionNotification();
}
$this->info('Daily maintenance completed.');
return self::SUCCESS;
}
private function processAnalytics(): void
{
$this->info('Processing daily analytics...');
$tenants = Tenant::active()->get();
$bar = $this->output->createProgressBar($tenants->count());
foreach ($tenants as $tenant) {
GenerateReportJob::dispatch($tenant, 'daily', 'json');
$bar->advance();
}
$bar->finish();
$this->newLine();
}
private function cleanupExpiredData(): void
{
$this->call('shopmetrics:cleanup', ['--days' => 90]);
}
private function syncSubscriptions(): void
{
$this->call('shopmetrics:sync-subscriptions');
}
private function sendCompletionNotification(): void
{
$admin = User::where('role', 'super_admin')->first();
$admin?->notify(new DailyMaintenanceCompleted());
}
}
// Schedule it
// routes/console.php
Schedule::command('shopmetrics:daily-maintenance --notify')
->dailyAt('02:00')
->onOneServer()
->withoutOverlapping()
->emailOutputOnFailure('ops@shopmetrics.io');
❓ FAQ
make:command and make:command --command?--command=xxx Specifies a custom command name: php artisan make:command SendEmails --command=emails:send. If omitted, the default name app:console-commands:send-emails is used. It is recommended to always specify a custom command name.* * * * * php artisan schedule:run. All tasks are defined in the code, making them version-controlled and testable. Do not set up a separate Cron job for each task.$this->artisan('command:name', ['arg' => 'value']) to execute the command in a test, and assert that the output is ->expectsOutput('Done') or verify the database changes.--dry-run option to preview the results before execution.->appendOutputTo(storage_path('logs/command.log')) to append logs in the scheduler; use Log::info() within the command; or use emailOutputOnFailure() to send an email notification in case of failure.📖 Summary
- Artisan includes over 100 built-in commands covering operations such as migrations, caching, queues, and routing.
- Custom commands use signature syntax to define parameters and options
- Use Laravel Schedule to automate command scheduling instead of manual Cron jobs
- Interactive commands use
ask,confirm, andchoiceto provide a guided experience - Table output and progress bars make the commands look more professional
- onOneServer/withoutOverlapping: Prevents multiple instances from running simultaneously
📝 Exercises
-
Basic Question (⭐): Create a command named
shopmetrics:tenant-statsthat accepts atenantparameter and outputs the number of stores, orders, and total revenue for that tenant, formatted using thetablecommand. -
Advanced Exercise (⭐⭐): Create the
shopmetrics:daily-maintenancecommand, which includes analytics processing (with a progress bar) + cleanup of expired data + subscription synchronization, and configure the Schedule to run automatically at 2:00 a.m. every day. -
Challenge (⭐⭐⭐): Implement an interactive
shopmetrics:tenant-setupwizard command—enter the tenant name, slug, plan, and administrator email address sequentially, with validation at each step, and finally confirm creation. Include auto-complete (anticipate) suggestions for the tenant name.



