In Laravel 13, how do you create a custom Artisan command with `make:command`, and how are its signature, description and `handle()` wired up?
answer
- a class in app/Console/Commands
- #[Signature] and #[Description] attributes
- $signature property still accepted
- handle() parameters come from the container
- auto-registered; closures in routes/console.php
basics
~10 sphp 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
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
Recall make:command, where the class lands, and what the signature, description and handle() are for.
Explain attribute versus property signatures, container injection into handle(), and how discovery and withCommands() register commands.
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.
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.