Reader Stacks

Creating a Custom Artisan Command in Laravel

How to scaffold, argument/option-flag, and register a custom php artisan command — including the one config step people forget when it does not show up in the command list.

Creating a Custom Artisan Command in Laravel

Custom Artisan commands are the right tool for anything that needs to run outside a web request — a nightly cleanup job, a one-off data fix, a scheduled report — and Laravel makes creating one a single command.

1. Generate the command

php artisan make:command PruneStaleInvites

This creates app/Console/Commands/PruneStaleInvites.php with two properties you'll edit: $signature (how it's called) and $description (what shows in php artisan list).

2. Define the signature, including arguments and options

protected $signature = 'invites:prune {--days=30 : Delete invites older than this many days}';

protected $description = 'Delete pending invites older than the given number of days.';

public function handle(): int
{
    $days = (int) $this->option('days');

    $count = Invite::where('status', 'pending')
        ->where('created_at', '<', now()->subDays($days))
        ->delete();

    $this->info("Deleted {$count} stale invites older than {$days} days.");

    return self::SUCCESS;
}

Run it:

php artisan invites:prune --days=14

Why it might not show up in `php artisan list`

On a fresh Laravel install, commands placed in app/Console/Commands/ are auto-discovered — no manual registration needed. If a command genuinely doesn't appear, the usual cause is a syntax error in the file (check php artisan list output for a warning) or, on an older project still using the pre-Laravel-11 Console/Kernel.php structure, a missing entry in that file's commands() method if auto-discovery was disabled there.

Scheduling it

Once it works manually, scheduling it is one line — in Laravel 11+, in routes/console.php or bootstrap/app.php's withSchedule(); in Laravel 10 and earlier, in Console/Kernel.php's schedule() method:

Schedule::command('invites:prune --days=30')->daily();

Arguments and options solve different command-line problems

An argument is positional and usually identifies the thing the command acts on; an option is named and is better for modifiers or flags. A command that targets one account but optionally performs a dry run could use both:

protected $signature = 'accounts:rebuild
    {account : Account ID}
    {--dry-run : Show changes without writing them}';
$accountId = (int) $this->argument('account');
$dryRun = (bool) $this->option('dry-run');

That makes invocation self-documenting: php artisan accounts:rebuild 42 --dry-run.

Return a failure code when the command did not complete

Console output is for humans; the process exit code is for cron, CI, deployment scripts, and other programs. If a required record is missing or an operation fails in a way the command handles itself, return self::FAILURE rather than printing an error and still exiting successfully:

if (! $account) {
    $this->error('Account not found.');

    return self::FAILURE;
}

A scheduler or shell wrapper can then detect the failure without scraping text output.

Keep the command thin when the same operation exists elsewhere

If pruning invites is also triggered from an admin screen or a queue job, move the actual pruning logic into an application service and let the command call that service. Artisan commands are entry points, just like controllers — putting all business logic directly in handle() makes the behavior harder to reuse and test.

Be careful with destructive commands in production

For one-off data repairs, a --dry-run option, a clear count of affected rows, and an interactive confirmation can prevent expensive mistakes. Scheduled commands should not prompt interactively; make those idempotent and safe to run unattended instead.

Topics: Developer Productivity