skip to content

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%

answer

  1. curly braces around every input
  2. ? optional, =value default, * array
  3. --flag switch returns true or false
  4. --opt= value is a string or null
  5. --X|opt shortcut, --id=* repeats

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.

solid answer

~40 s

Every input sits in braces after the command name. Arguments are positional: `{customer}` is required, `{customer?}` optional, `{customer=42}` optional with a default, `{customer*}` one or more values as an array and `{customer?*}` zero or more. Options start with `--`: `{--dry-run}` is a switch, so `$this->option('dry-run')` is `true` or `false`; `{--since=}` takes a value and is `null` when omitted; `{--limit=50}` defaults to `'50'`; `{--I|invoice=}` adds the shortcut `-I`; `{--invoice=*}` collects repeated `--invoice=17 --invoice=18` into an array. Text after ` : ` is the description shown by `help`. Values arrive as **strings**, so cast them or, from Laravel 13.18.1, use `$this->input()->integer('limit')`. A missing required argument is an error unless the command implements `PromptsForMissingInput`, which asks for it interactively.

code

php · 23 lines
php
<?php

use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;

#[Signature('invoices:resend-failed
    {customer : The customer ID}
    {--since= : Only invoices that failed after this date}
    {--L|limit=50 : Maximum emails to send}
    {--invoice=* : Specific invoice IDs}
    {--dry-run : List the invoices without sending}')]
class ResendFailedInvoices extends Command
{
    public function handle(): int
    {
        $limit = $this->input()->integer('limit');   // 50, not '50'
        $ids = $this->option('invoice');             // array of strings
        $dryRun = $this->option('dry-run');          // true or false

        // ...
        return self::SUCCESS;
    }
}

go deeper

for a junior

Recall the brace syntax: ? for optional, = for defaults, * for arrays, -- for options, | for shortcuts, : for descriptions.

for a middle

Explain what argument() and option() return in each case — true/false, null, default strings, arrays — and why numeric input needs casting or typed accessors.

for a senior

Design signatures that make destructive paths explicit and self-documenting, and know when PromptsForMissingInput helps and when a non-interactive run makes it irrelevant.

for a principal

Set conventions for operational commands — naming, dry-run defaults, documented inputs — so any engineer can run another team's command safely.

## Where input is declared A Laravel command declares its whole input contract in its **signature** — the string in the `#[Signature]` attribute or the `$signature` property. The first word is the command's name; every argument and option after it sits in **curly braces**. Laravel parses the string into Symfony Console argument and option definitions, which is why `php artisan help` can print an accurate usage screen for every command without extra work. ## Arguments: positional input Arguments are matched by position, in the order they appear. | Signature fragment | Meaning | `$this->argument('customer')` when not passed | |---|---|---| | `{customer}` | required | error before `handle()` runs | | `{customer?}` | optional | `null` | | `{customer=42}` | optional with default | `'42'` | | `{customer*}` | one or more values | error before `handle()` runs | | `{customer?*}` | zero or more values | `[]` | Array arguments must come last, since they swallow every remaining positional value: `php artisan invoices:resend-failed 42 43 44` gives `['42', '43', '44']`. ## Options: named input Options are prefixed with two hyphens on the command line and may appear in any order. - `{--dry-run}` — a **switch**. `$this->option('dry-run')` is `true` when passed and `false` otherwise. - `{--since=}` — takes a value: `--since=2026-09-01`. Omitted, the option is `null`. - `{--limit=50}` — takes a value with a default: omitted, the option is `'50'`. - `{--I|invoice=}` — the part before `|` is a one-letter **shortcut**, used with a single hyphen and no `=`: `-I17`. - `{--invoice=*}` — an **array option**: `--invoice=17 --invoice=18` gives `['17', '18']`; omitted, it is an empty array. - `{--queue= : Queue to use}` — anything after ` : ` (space, colon, space) is the **description** shown on the help screen; this works for arguments too. ## Reading the values 1. `$this->argument('customer')` and `$this->option('limit')` return single values; `$this->arguments()` and `$this->options()` return everything as arrays. 2. Values from the command line are **strings**. `--limit=50` is `'50'`, not `50`, so strict comparisons and typed parameters need a cast. 3. From Laravel 13.18.1, `$this->input()` returns a `CommandInput` object with the typed helpers you know from requests — `integer('limit')`, `boolean('dry-run')`, `date('since')`, `enum('status', InvoiceStatus::class)` — and `$this->input('queue', 'default')` reads one value with a fallback. ## Missing required input By default a missing required argument stops the command before `handle()` with a "not enough arguments" error. If the class implements `Illuminate\Contracts\Console\PromptsForMissingInput`, Laravel instead **asks** for each missing required argument in an interactive terminal, phrasing the question from the argument's name or description; `promptForMissingArgumentsUsing()` customises the questions. In a non-interactive run there is no one to ask, so the argument is still missing. ## A worked example With the signature `invoices:resend-failed {customer} {--since=} {--L|limit=50} {--invoice=*} {--dry-run}`: | Command line | `customer` | `since` | `limit` | `invoice` | `dry-run` | |---|---|---|---|---|---| | `invoices:resend-failed 42` | `'42'` | `null` | `'50'` | `[]` | `false` | | `invoices:resend-failed 42 --since=2026-09-01 -L10` | `'42'` | `'2026-09-01'` | `'10'` | `[]` | `false` | | `invoices:resend-failed 42 --invoice=17 --invoice=18 --dry-run` | `'42'` | `null` | `'50'` | `['17', '18']` | `true` | Every value that came from text is a string; only the switch is a real boolean. ## Designing a good signature - Make the dangerous path explicit: prefer a `--dry-run` switch or a required `--confirm`-style option over a destructive default. - Put identifiers in arguments and tuning knobs in options: `invoices:resend-failed {customer} {--since=} {--limit=50} {--dry-run}`. - Describe every input with ` : ` so `php artisan help` documents the command for you. - Keep names kebab-case (`--dry-run`), and read them with the same spelling: `$this->option('dry-run')`. ## Common mistakes - Writing `{--limit}` and expecting a value: that is a switch, and `--limit=50` is then rejected because the option accepts no value. - Comparing `$this->option('limit') === 50` and never matching, because the value is the string `'50'`. - Putting an optional argument before a required one, which Symfony Console rejects when the command is built.

  • What does `$this->option('since')` return for `{--since=}` when the user types `--since` with no value?
    `null`. A `{--since=}` option is defined as value-optional, so the flag may appear without a value, and then it holds no value, the same as when it is omitted. `hasOption()` will not tell the two apart — it only reports whether the option is defined — so if 'passed without a value' must mean something, redesign the option, for example with a meaningful default.
  • How would you accept several invoice IDs for one customer in the re-send command?
    Either an array option, `{--invoice=*}`, called as `--invoice=17 --invoice=18` and read as `['17', '18']`, or an optional array argument after the customer, `{customer} {invoice?*}`, called as `42 17 18`. The option form keeps the call self-describing and lets the customer argument stay first; either way the IDs arrive as strings.

saying these in an interview costs you the question

  • {--dry-run} requires a value, so users must type --dry-run=true.
  • Option values arrive as PHP integers when the user types numbers.
  • {customer?} makes the option --customer optional.
  • A missing required argument always makes Laravel prompt for it.
  • Shortcuts are written with two hyphens, like --I17.