skip to content

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.