skip to content

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%

answer

  1. interactive needs a TTY on STDIN
  2. non-interactive: each prompt returns its default
  3. Prompts confirm() defaults to true
  4. required prompt with no default throws
  5. options for every answer; default: false

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.

solid answer

~40 s

Laravel Prompts — `text()`, `select()`, `multiselect()`, `search()`, `confirm()` and friends — only render when the command is interactive: Laravel enables them when the input is interactive and `STDIN` is a TTY. From cron, a CI job, a pipe, or with `--no-interaction`, each prompt skips the question and **returns its default**, after running its validation. `confirm()` defaults to `true`, so `confirm('Re-send 40 invoice emails?')` silently answers yes and the emails go out. A required prompt whose default fails validation throws `NonInteractiveValidationException` instead. So design for both modes: make every answer available as an argument or option, prompt only when it is missing, pass `default: false` to any destructive `confirm()`, and give unattended runs an explicit `--force`-style switch. `PromptsForMissingInput` also asks only in interactive runs.

go deeper

for a junior

Recall that Laravel Prompts provides text, select, search and confirm, and that --no-interaction stops commands from asking.

for a middle

Explain the interactivity check (interactive input plus a TTY on STDIN) and that each prompt returns its validated default when it cannot ask.

for a senior

Design commands that are safe unattended: flags for every answer, default: false on destructive confirms, a --force switch, and the confirm() default trap called out.

for a principal

Set a team rule that operational commands must run safely both by hand and unattended, and review new commands against it.

## What Laravel Prompts is **Laravel Prompts** (`laravel/prompts`, still a 0.x package, required by the framework) provides the browser-like terminal forms Laravel's own commands use: `text()`, `textarea()`, `number()`, `password()`, `select()`, `multiselect()`, `suggest()`, `search()`, `multisearch()`, `confirm()`, `pause()`, plus output helpers such as `spin()`, `progress()`, `table()`, `note()` and `info()`. They are plain functions in the `Laravel\Prompts` namespace, usable in any command: ```php use function Laravel\Prompts\confirm; use function Laravel\Prompts\select; ``` They add placeholders, validation, hints and keyboard navigation that the older `$this->ask()` and `$this->choice()` helpers lack. ## When a prompt actually asks Before a command runs, Laravel configures Prompts for it. A prompt renders interactively only when: 1. the command's input is interactive — `--no-interaction` / `-n` turns that off; **and** 2. `STDIN` exists and is a terminal (a TTY). That second condition is false under cron, in most CI jobs, when input is piped, and inside a web request, where PHP defines no `STDIN` at all — so a command started with `Artisan::call()` from a controller is non-interactive too. (During automated tests Laravel forces Prompts to interactive mode and falls back to Symfony's question helper so tests can answer them; on native Windows Prompts also uses that fallback.) ## What a prompt returns when it cannot ask In non-interactive mode a prompt **does not wait and does not fail by itself**. It takes its **default value**, runs the same validation it would run on typed input, and: - returns the default if it passes; - throws `Laravel\Prompts\Exceptions\NonInteractiveValidationException` if it fails — for example a `text(required: true)` whose default is an empty string. ## The `confirm()` trap | Helper | Default answer | |---|---| | `Laravel\Prompts\confirm($label)` | `true` | | `$this->confirm($question)` (older command helper) | `false` | | the framework's own production confirmation for migrations | asks with `default: false` | So this code, run from cron, re-sends every email without anyone agreeing: ```php if (confirm("Re-send {$count} invoice emails for customer {$id}?")) { $mailer->resendFailedFor($customer); } ``` The operator who tested it by hand always saw the question; the scheduler never did. ## Designing for both modes - **Every answer has a flag.** Anything you prompt for should also be an argument or option — `{customer?}`, `{--since=}` — and you prompt only when it is missing. - **Destructive confirmations default to no.** Write `confirm('Re-send ...?', default: false)` and require an explicit `--force` switch for unattended runs. - **Check the mode when it matters.** `$this->input->isInteractive()` tells you whether a human can answer; refuse to proceed, or print a dry-run table, when they cannot and no `--force` was given. - **Use `PromptsForMissingInput` for arguments.** It asks for missing required arguments only in interactive runs; unattended, the missing argument is still an error. - **Validate the same way in both paths.** Give prompts `validate:` rules and apply the same rules to flag values. ## Why interviewers ask This is a production-judgement question disguised as a syntax one. Most developers test a command by running it in their own terminal, where every prompt appears and every confirmation is answered by a human. The difference only shows up later, when the same command is scheduled, run from a deploy pipeline or triggered from an admin screen — and then an unasked `confirm()` quietly answers yes. A strong candidate knows the interactivity rule, the default-return behaviour and the `confirm()` default, and designs commands whose unattended behaviour is chosen, not accidental. ## A safe shape for the re-send command 1. Read `customer` from the argument, or `search()` for one if it is missing and the run is interactive. 2. Build the list of failed invoices and show it with `table()`. 3. If `--dry-run`, stop there. 4. If interactive, `confirm(..., default: false)`; if not interactive, require `--force`. 5. Re-send, report a summary, and return `self::FAILURE` if any email still failed.

  • Why is a command started with `Artisan::call()` from a controller non-interactive even without `--no-interaction`?
    Laravel only enables interactive Prompts when the input is interactive and `STDIN` is defined and is a TTY. In a web request PHP does not define `STDIN`, so every prompt returns its default or throws if the default fails validation. Web-triggered commands must receive all their answers as arguments and options.
  • What does `text(label: 'Customer ID', required: true)` do when the command runs with `--no-interaction`?
    It cannot ask, so it validates its default — an empty string — against `required`, the validation fails, and it throws `NonInteractiveValidationException`. The command stops with that error rather than continuing with an empty ID, which is the safe outcome; supplying the value as an argument avoids it.

A prompt without a terminal is like a self-checkout that times out on 'Do you need a receipt?': it silently applies whatever the default button says, so the default must be the answer you can live with when nobody is there.

saying these in an interview costs you the question

  • Prompts block forever waiting for input when run from cron.
  • confirm() from Laravel Prompts defaults to false, like $this->confirm().
  • --no-interaction makes every prompt return null.
  • PromptsForMissingInput asks for missing arguments even in non-interactive runs.
  • A command called with Artisan::call() inside a web request can still ask the user questions.