skip to content

Custom Console Commands

A custom Artisan command declares its arguments in a $signature, does its work in handle() with injected services, and asks questions through Laravel Prompts. Interviewers probe input and output.

on this pageshow

explore

questions

6

In Laravel 13, how do you create a custom Artisan command with `make:command`, and how are its signature, description and `handle()` wired up?

level: juniorimportance: must knowfreq 65%

answer

  1. a class in app/Console/Commands
  2. #[Signature] and #[Description] attributes
  3. $signature property still accepted
  4. handle() parameters come from the container
  5. auto-registered; closures in routes/console.php

basics

~10 s

php artisan make:command ResendFailedInvoices writes a class to app/Console/Commands, which Laravel registers automatically. Its #[Signature] attribute names the command and declares input, #[Description] feeds the listing, and handle() does the work with container-injected services.

solid answer

~40 s

`php artisan make:command ResendFailedInvoices` creates `app/Console/Commands/ResendFailedInvoices.php`. In Laravel 13 the generated class carries a `#[Signature('...')]` and a `#[Description('...')]` attribute; the older `protected $signature` and `$description` properties still work. The signature holds the command name — say `invoices:resend-failed {customer}` — plus its arguments and options, and the description is what `php artisan list` prints. Laravel calls `handle()` through the service container, so type-hinted services such as an `InvoiceMailer` are injected, while input is read with `$this->argument()` and `$this->option()`. Classes in `app/Console/Commands` are registered automatically; other folders are added with `withCommands()` in `bootstrap/app.php`. For something tiny, `Artisan::command()` in `routes/console.php` defines a closure command, described with `->purpose()`. I keep `handle()` thin and delegate to a service I can test and reuse.

code

php · 25 lines
php
<?php

namespace App\Console\Commands;

use App\Billing\InvoiceMailer;
use App\Models\Customer;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;

#[Signature('invoices:resend-failed {customer : The customer ID}')]
#[Description('Re-send failed invoice emails for one customer')]
class ResendFailedInvoices extends Command
{
    public function handle(InvoiceMailer $mailer): int
    {
        $customer = Customer::findOrFail($this->argument('customer'));

        $sent = $mailer->resendFailedFor($customer);

        $this->info("Re-sent {$sent} invoice email(s).");

        return self::SUCCESS;
    }
}

go deeper

for a junior

Recall make:command, where the class lands, and what the signature, description and handle() are for.

for a middle

Explain attribute versus property signatures, container injection into handle(), and how discovery and withCommands() register commands.

for a senior

Show design judgement: commands as thin entry points over services, meaningful names and descriptions, and no reliance on a Kernel class that new apps lack.

for a principal

Decide what belongs in a command versus a job or admin action, and how a team keeps its operational commands discoverable and reviewed.

## What a custom command is A **custom Artisan command** is a class the application adds to Laravel's command-line interface, so `php artisan invoices:resend-failed 42` runs your code with the whole framework booted: configuration, the container, Eloquent, mail, queues. Teams use them for maintenance and support tasks that must be repeatable, reviewed and runnable the same way on every environment — re-sending failed invoice emails for one customer, rebuilding a search index, pruning old exports. ## Generating the class ```bash php artisan make:command ResendFailedInvoices ``` The generator creates `app/Console/Commands/ResendFailedInvoices.php` (and the folder, the first time). In Laravel 13 the class it writes looks like this before you fill it in: - a `#[Signature('...')]` attribute holding the command's name and input definition; - a `#[Description('Command description')]` attribute; - an empty `handle()` method, which Laravel calls when the command runs. ## Signature and description: attributes or properties | Style | Name and input | Listing text | |---|---|---| | Laravel 13 attributes (what `make:command` generates) | `#[Signature('invoices:resend-failed {customer}')]` | `#[Description('Re-send failed invoice emails')]` | | Properties (older apps, still supported) | `protected $signature = 'invoices:resend-failed {customer}';` | `protected $description = '...';` | Both feed the same parser. The **name** is the first word of the signature — conventionally `namespace:action`, so it groups under `invoices` in `php artisan list` — and everything after it declares arguments and options. The description appears in `php artisan list` and on the `help` screen, so write it for a colleague who has never seen the command. `#[Signature]` also accepts an `aliases:` list, and a `#[Hidden]` attribute keeps a command out of the listing. ## `handle()` and dependency injection Laravel invokes `handle()` through the **service container**, exactly as it resolves a controller action: - type-hint any class or interface — `InvoiceMailer $mailer`, `LoggerInterface $log` — and it is resolved and injected when the command runs; - read input with `$this->argument('customer')` and `$this->option('dry-run')`; - write output with `$this->info()`, `$this->error()`, `$this->table()` and the other output helpers; - return an exit code, or return nothing for success. Injecting into `handle()` rather than doing heavy work in the constructor keeps resolution tied to the moment the command actually runs. ## Registration 1. Classes in `app/Console/Commands` are **discovered automatically**: a new Laravel 13 app configures command discovery for that folder by default, so there is no `app/Console/Kernel.php` to edit — that class no longer exists in new applications. 2. Commands elsewhere are added in `bootstrap/app.php` with `->withCommands([...])`, passing directories to scan or class names. 3. **Closure commands** live in `routes/console.php`: `Artisan::command('invoices:count-failed {customer}', function (string $customer) { ... })->purpose('Count failed invoices');`. The closure is bound to the command object, so `$this->info()` works inside it, and extra type-hinted parameters are injected. The skeleton's `inspire` command is one. ## Keep the command thin A command is an **entry point**, like a controller. Put the real work — finding the customer's failed invoices, re-sending them, recording the result — in a service class. Then: - the same logic can run from a queued job, an admin screen or a test without going through Artisan; - the command itself stays a readable list of steps: parse input, call the service, report, return a code; - feature tests can exercise the service directly and keep only a thin test around the command. ## Why interviewers ask The question checks two things at once. First, whether the candidate has worked in a current Laravel application: someone who immediately reaches for `app/Console/Kernel.php` learned on Laravel 10 or earlier and has not noticed the slim skeleton. Second, whether they treat commands as real application code — named clearly, described, injected with services, kept thin and tested — rather than as throwaway scripts. A strong answer names the generator, the attribute or property that declares the signature, automatic discovery, container injection into `handle()`, and the habit of delegating to a service. ## Common mistakes - Looking for `app/Console/Kernel.php` to register the command in a new app. - Putting all logic in `handle()`, so nothing else can reuse it. - Leaving the generated `'Command description'` text in place, which makes `php artisan list` useless as documentation.

  • How do you register commands that live outside `app/Console/Commands`?
    Pass them to `withCommands()` in `bootstrap/app.php`, for example `->withCommands([__DIR__.'/../app/Billing/Console'])`. Directories are scanned for command classes and class names are registered directly. A package registers its commands from its service provider with `$this->commands([...])` instead.
  • When would you choose a closure command in `routes/console.php` over a class?
    For a tiny, local task with little input — the skeleton's `inspire` command is the model. `Artisan::command()` takes the signature and a closure, `->purpose()` sets the description, and the closure can use `$this->info()` because it is bound to the command. Anything with several options, tests or reuse deserves a class; a closure command also cannot implement an interface such as `Isolatable`.

saying these in an interview costs you the question

  • Custom commands must be registered in app/Console/Kernel.php before they run.
  • The command's name comes from the class name rather than from the signature.
  • handle() cannot take dependencies, so services must be fetched with app() inside it.
  • The $signature property stopped working once Laravel 13 introduced #[Signature].
  • Closure commands in routes/console.php are HTTP routes reachable from the browser.
open as a page

In a Laravel command signature, how do you declare required, optional, defaulted and array arguments, and switch versus value options with shortcuts?

level: middleimportance: must knowfreq 45%

basics

~20 s

In a Laravel signature, {customer} is required, {customer?} optional, {customer=5} defaulted and {customer*} an array. {--dry-run} is a true/false switch, {--limit=} takes a string value, {--limit=50} has a default, {--L|limit=} adds a shortcut and {--id=*} repeats.

open as a page

How should a Laravel console command report progress, results and failure using `table()`, `withProgressBar()`, `fail()` and its return value?

level: middleimportance: should knowfreq 40%

basics

~20 s

A Laravel command prints with info(), warn() and error(), shows rows with table() and long loops with withProgressBar(), and signals failure through its exit code: return self::FAILURE or call fail(). Printing an error alone still exits 0.

open as a page

What happens when a Laravel controller runs `Artisan::call('invoices:resend-failed', [...])`, and when should you use `Artisan::queue`, `$this->call` or `callSilently` instead?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Artisan::call() runs the command synchronously inside the current request, returns its exit code, buffers its output for Artisan::output() and lets exceptions propagate. Artisan::queue() runs it later on a worker; inside a command, $this->call() and callSilently() run another command.

open as a page

How does implementing `Isolatable` on a Laravel command prevent overlapping runs, and what should `isolatableId()` return for a per-customer invoice re-send command?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Implementing Isolatable adds an --isolated option; with it, Laravel takes an atomic cache lock named after the command before handle() runs and skips the run if the lock is held. isolatableId() should return the customer ID so different customers can run at once.

open as a page

A Laravel command asks `confirm('Re-send 40 invoice emails?')` through Laravel Prompts; what happens when it runs from cron or with `--no-interaction`, and how should you design it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Without an interactive terminal, Laravel Prompts does not ask: each prompt returns its default after validating it. confirm() defaults to true, so an unattended run answers yes; a required prompt with no usable default throws NonInteractiveValidationException.

open as a page