Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Prompts Laravel Package

laravel/prompts

Laravel Prompts is a PHP package for adding beautiful, user-friendly forms to command-line apps, with browser-like features including placeholder text and validation. Perfect for Artisan console commands or any CLI PHP project.

View on GitHub
Deep Wiki
Context7

Getting Started

Install / confirm availability

In a Laravel 10+ app the package is already a transitive dependency of laravel/framework, so there is nothing to install — check with:

composer show laravel/prompts

For non-Laravel CLI projects (it is framework-agnostic, only PHP 8.x + Symfony console-ish runtime):

composer require laravel/prompts

The global helper functions (text(), select(), progress(), …) are registered through Composer's files autoload, so they are available immediately without any service provider.

Where to look first

  1. The official docs page (laravel.com/docs/prompts) — this is the canonical API reference; the repo README is intentionally minimal.
  2. src/helpers.php — one function per prompt type, with the full parameter list (this is the real "cheat sheet").
  3. src/Prompt.php — the base class: fallbacks (fallbackWhen, fallbackUsing), interactivity (interactive()), output (setOutput()), and the testing harness (fake(), assertPrompted(), assertNothingPrompted(), assertSettled()).
  4. src/Concerns/ — CanValidate, CanTransform, HasHints, InteractsWithIO, Scrolling etc. Most "why does it behave like that?" questions are answered here.
  5. changelog/ — useful because the package is still 0.x; APIs are added often (number() in 0.3.11, grid() in 0.3.10, conditional form steps in 0.3.1).

First use case

Take an existing php artisan command that uses $this->ask() / $this->confirm() and upgrade one question. The value is immediate: validation, placeholder, default, hint, and a rendered "answered" state for free.

use function Laravel\Prompts\text;

public function handle(): int
{
    $name = text(
        label: 'What is your project name?',
        placeholder: 'My Awesome Project',
        default: 'my-project',
        hint: 'Used for the directory name and namespace.',
        required: 'A project name is required.',
        validate: fn (string $value) => match (true) {
            strlen($value) > 60 => 'The name must be 60 characters or fewer.',
            ! preg_match('/^[a-z0-9-_]+$/i', $value) => 'Only letters, numbers, dashes and underscores.',
            default => null,
        },
        transform: fn (string $value) => strtolower($value),
    );

    return self::SUCCESS;
}

That single call exercises the mental model for everything else: label → raw value → validate → transform → returned value.


Implementation Patterns

1. Prompt-per-question inside an Artisan command

The bread-and-butter pattern. Keep prompts in handle()/interact() (not in constructors, not in the service layer), and push the resulting values into a service/action class:

$email    = text('Email address', required: true, validate: fn ($v) => filter_var($v, FILTER_VALIDATE_EMAIL) ? null : 'Invalid email.');
$password = password('Password', required: true, validate: fn ($v) => strlen($v) < 12 ? 'At least 12 characters.' : null);
$role     = select('Role', ['admin', 'editor', 'viewer'], default: 'editor');
$perks    = multiselect('Perks', ['parking', 'lunch', 'gym'], default: ['lunch'], required: true);
$notify   = confirm('Send a welcome email?', default: true, yes: 'Yes', no: 'No', hint: 'Can be changed later.');

Use Artisan::call()-free composition: the command's job is only to collect and validate input; everything after is plain PHP.

2. Keyed select — treat the array key as your domain value

$status = select(
    label: 'Status',
    options: [
        'draft'     => 'Draft',
        'published' => 'Published',
        'archived'  => 'Archived',
    ],
    default: 'draft',
);
// $status === 'draft' (the KEY, not the label)

This is the cleanest way to prompt for enum-ish values — map the key in the prompt, never post-process the label string.

3. The form() API for multi-step wizards

When you have 4+ related questions (installers, make:* generators, setup wizards), use form() instead of a wall of independent prompts. Steps are named, responses come back as an associative array, and the whole thing renders as one coherent flow:

use function Laravel\Prompts\form;

$responses = form()
    ->text('Application name', name: 'name', required: true, default: 'Laravel')
    ->select('Database', ['mysql', 'pgsql', 'sqlite'], name: 'driver', default: 'sqlite')
    ->when(
        fn (array $responses) => $responses['driver'] !== 'sqlite',
        fn ($form) => $form->text('Host', name: 'host', default: '127.0.0.1')
                          ->text('Database name', name: 'database'),
    )
    ->confirm('Run migrations now?', name: 'migrate', default: true)
    ->submit();

$responses['name'];      // string
$responses['migrate'];   // bool

Tips that pay off in practice:

  • Give every step a name: — the returned array is only as useful as its keys, and step names are passed through since v0.3.5.
  • Conditional steps (v0.3.1) are how you avoid asking Postgres questions to a SQLite user.
  • Do the side effects after submit(). If a mid-form step throws, the user's earlier answers are lost, which is the correct behaviour to keep the flow re-runnable.

4. Long-running work: spin() and progress()

$count = spin(
    callback: fn () => User::query()->update(['active' => true]),
    message: 'Activating users…',
);
$progress = progress(label: 'Importing rows', steps: $rows->count());
$progress->start();

foreach ($rows as $row) {
    $progress->advance();
}

$progress->finish();
  • spin() returns whatever the callback returns and propagates exceptions (the spinner is torn down on failure — fixed back in 0.1.8).
  • progress() renders in place; always call finish() so the bar doesn't bleed into subsequent output. Re-rendering during the callback is supported (0.1.24), so you can call $progress->advance() from inside a callback-driven loop.

5. Non-interactive / scripted execution

This is the pattern most teams forget, then get bitten in CI:

use Laravel\Prompts\Prompt;

Prompt::fallbackWhen(
    fn () => ! $this->input->isInteractive() || app()->runningUnitTests()
);

Prompt::fallbackUsing(
    fn ($prompt) => $this->components->ask($prompt->label) ?? $prompt->default
);
  • --no-interaction (and --quiet, plus non-TTY stdin) can be handled by returning each prompt's default.
  • Always pass default: on prompts in commands that might run in CI/CD, containers, or schedulers. "No interactive input" + "no default" is the single most common production failure mode for this package.
  • Prefer fallbackWhen() over sprinkling if ($this->option('no-interaction')) branches through the command.

6. Testing

Two complementary strategies:

A. expectsQuestion — the lowest-friction approach, and it works with prompts because they read from the console input stream:

$this->artisan('user:create')
    ->expectsQuestion('Email address', '[email protected]')
    ->expectsQuestion('Password', 'secret-but-long-enough')
    ->expectsQuestion('Role', 'admin')        // select: pass the KEY
    ->expectsConfirmation('Send a welcome email?', 'yes')
    ->assertSuccessful();

Caveat: the string you pass must match the rendered label, so keep labels static and free of dynamic values (or assert with a substring-friendly Prompt::fake() approach instead).

B. Prompt::fake() — purpose-built, supports output assertions and introspection:

use Laravel\Prompts\Prompt;

Prompt::fake(['Taylor', '[email protected]', 'admin']);

$this->artisan('user:create')->assertSuccessful();

Prompt::assertPrompted(fn ($prompt) => str_contains($prompt->label, 'Email'));
Prompt::assertOutputContains('User created.');
Prompt::assertSettled();   // ensures nothing was left hanging
  • Pass false in the fake response array to simulate Ctrl+C / cancellation.
  • Prompt::fake() also lets you assert against prompts that never rendered with assertNothingPrompted() — great for "should skip the wizard entirely when --force is passed".
  • For form(), fake the responses in step order and assert the resulting side effects, not the individual prompts.

7. Output primitives (note, table, grid, alert, clear)

These are the "polish" layer that makes a hand-rolled CLI feel like an official Artisan command:

note('The build cache was cleared.', 'Cache');
info('Using the local configuration file.');
warning('This will delete 3 records.');
error('Deployment failed.');
alert('Are you sure you want to continue?', 'Destructive operation');

table(
    headers: ['Name', 'Status', 'Last run'],
    rows: $jobs->map(fn ($j) => [$j->name, $j->status, $j->last_run_at->diffForHumans()])->all(),
);

clear() (v0.2.0) is handy between wizard phases when you don't want the user scrolling back through stale questions.

8. Custom prompts (extension point)

If you need a new widget, extend Laravel\Prompts\Prompt rather than bending text():

class ColorPrompt extends Prompt
{
    public function value(): string { /* … */ }
    protected function display(): void { /* … */ }
    public function prompt(): mixed { /* … */ }
}

Then optionally wrap it in a helper function guarded by function_exists() (the package itself had to add that guard in 0.1.22 — do the same to avoid collisions).


Gotchas and Tips

Interactivity & terminal requirements

  • Prompts require a real TTY. In Docker you need docker run -it; without it you get read failures or hangs. Under CI, stdin is not a TTY and non-interactive mode kicks in.
  • The package shells out to stty and tput on Unix; minimal images (Alpine, distroless, some CI runners) may not have them. There's a fallback implementation, but the reliable fix is Prompt::fallbackWhen(fn () => ! Terminal::supports... ) or simply checking $this->input->isInteractive().
  • COLUMNS drives wrapping/truncation. If output looks misaligned in a piped/recorded environment, set COLUMNS=80 explicitly.

Validation quirks

  • validate must return null for valid and a string message for invalid. Returning false/true will not behave as you expect.
  • Throwing inside validate aborts the prompt entirely (exception bubbles out of the command) — useful for "cannot continue" conditions, dangerous if you meant "re-ask".
  • required accepts a bool or a custom message string. For select/search the custom "required" message has been supported since 0.1.9 and is much friendlier than the default.
  • transform runs after validation, so your validator sees the raw typed value. Normalise in transform (lowercasing, trimming) but validate the input as the user typed it — otherwise error messages become confusing.
  • Validation closures should be pure and cheap; they re-run on every keystroke submission, and network/DB calls inside them make the UI feel laggy.

Return values

  • select() / multiselect() / search() / multisearch() return keys, not labels. With a list-style array (['Red', 'Green']) you get integers 0/1, which is nearly always a bug in your code, not the package's.
  • multiselect() returns an array of keys — sort/normalise it yourself if key order matters downstream.
  • confirm() returns bool; it respects default: true in non-interactive mode.
  • number() (added in 0.3.11) returns a numeric type — do not assume string, unlike text().

Non-interactive mode

  • If stdin isn't a TTY and no default is supplied, you'll get an error rather than a silent hang — but it fails at the point of prompting, which can be deep inside a deployment. Audit all prompts in commands that run from cron/CI and give each one a default, or register a global fallbackUsing().
  • Fallbacks take priority over non-interactive mode (0.1.10), so a fallbackUsing() handler that itself prompts via $this->components->ask() will still block in CI. Guard the fallback too.
  • Prompt::interactive(false) is a blunt global switch — handy in tests, risky in application code because it's static and leaks across the process.

Testing pitfalls

  • Call Prompt::fake() before invoking the command. Faking after the first prompt has rendered gives you the real prompt.
  • expectsQuestion() matching is literal against the rendered label; hints, placeholders and defaults are rendered around it but are not part of what you match. Keep labels short and constant.
  • If a test "passes" but leaves output mangled, you probably missed a response — Prompt::assertSettled() and assertNothingPrompted() catch leftover/unexpected prompts.
  • Testing spin()/progress(): fake them like any other prompt; asserting on spinner frames is brittle, assert on the callback's effects instead.

Output & rendering

  • Prompt labels accept Laravel console formatting tags, but never interpolate untrusted user data into a label — the package strips C0 control codes (0.1.4) yet ANSI/tag injection is still a real risk in multi-tenant CLIs.
  • Mixing $this->components->info() (Artisan's own components) with Prompts output can produce out-of-order lines because Prompts writes directly to the output in place. Prefer one system per command, or Prompt::setOutput($this->output) so both share the same buffered output.
  • Box drawing with multibyte/CJK/emoji input has had multiple fixes (0.1.4, 0.3.9); if you're on an old 0.x version, alignment bugs are likely already fixed upstream — upgrade before hacking around them.
  • Call clear() deliberately. It clears the terminal, not Laravel's output buffer — it will also erase anything your command printed before it (including exception traces you were about to show).

Version & dependency notes

  • This is a 0.x package with an active API surface. Pin a version range in non-Laravel projects and read the changelog before minor bumps; new prompt types (number, grid, conditional form steps) land in patch/minor releases.
  • The global helper functions are a namespace hazard: if you define text() or select() in the global namespace yourself, the Composer files autoload will collide. The package guards with function_exists(), but your own code should not add new global functions.
  • Because it ships with the framework, don't composer require it in a Laravel app just to bump it — constrain via the framework or add an explicit requirement matching the framework's constraint, otherwise you can force a conflict on the next laravel/framework upgrade.

Overall day-to-day verdict For Laravel console work this is close to a drop-in quality upgrade: the API is small, typed, and predictable, and the fallback/testing hooks are the parts that separate a demo command from something you can ship in CI. The learning investment is roughly one afternoon; the recurring cost is remembering to set default: everywhere and to assert on keys rather than labels.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
bernskioldmedia/laravel-matomo
bernskioldmedia/laravel-harvest
bernskiold/laravel-data-scrubber
bernskiold/laravel-currency-converter
bernskiold/laravel-campaign-monitor
bernskioldmedia/laravel-autotranslate
siberfx/telegram-bot
roy-sandip/laravel-view-extended
gregsphotography/site-builder-core
roy-sandip/laravel-view
anodyne/blade-fluentui-icons
webx-ui/module-catalog-collections
webx-ui/module-catalog-feeds
makidizajnerica/laravel-dto
matviib/scheduler
webx-ui/templates
anodyne/blade-tabler-icons
bref/laravel-websockets
anodyne/blade-hugeicons
1994/flatpack-statamic