CakePHP Prompts Plugin
Introduction
CakePHP Prompts wraps Laravel Prompts for CakePHP Console applications. It adds beautiful, user-friendly forms to the command line, with browser-like features including placeholder text and validation.
CakePHP Prompts is perfect for accepting user input in Cake Console commands. The plugin ships Cake Console helpers under the Crustum/Prompts.* namespace. Each helper maps to an upstream Laravel Prompts entry point and calls into laravel/prompts at runtime.
NOTE
CakePHP Prompts supports macOS, Linux, and Windows with WSL. On native Windows PHP, interactive TTY prompts fall back to CakePHP ConsoleIo. See unsupported environments & fallbacks.
This package does not ship Laravel's global helpers.php function API (text(), select(), …). Use Console helpers instead:
$name = $this->io->helper('Crustum/Prompts.Text')->run([
'label' => 'What is your name?',
'required' => true,
]);Prefer run(array $args): mixed when you need the return value. Cake's output(array $args): void contract also runs the same prompt and discards the result. Helpers bind ConsoleIoFallbacks automatically. Option lists may be plain arrays or Cake\Collection\Collection.
Console Helpers
| Helper name | Purpose |
|---|---|
Crustum/Prompts.Text | Single-line text input |
Crustum/Prompts.Textarea | Multi-line text input |
Crustum/Prompts.Number | Numeric input |
Crustum/Prompts.Password | Masked password input |
Crustum/Prompts.Confirm | Yes/no confirmation |
Crustum/Prompts.Select | Single option from a list |
Crustum/Prompts.MultiSelect | Multiple options from a list |
Crustum/Prompts.Suggest | Text with filtered suggestions |
Crustum/Prompts.Search | Searchable single select (options Closure required) |
Crustum/Prompts.MultiSearch | Searchable multi select (options Closure required) |
Crustum/Prompts.AutoComplete | Text with ghost-text completion |
Crustum/Prompts.Pause | Wait for Enter |
Crustum/Prompts.DataTable | Interactive searchable table |
Crustum/Prompts.Form | Returns a Laravel\Prompts\FormBuilder instance |
Crustum/Prompts.Note | Styled note (message, optional type) |
Crustum/Prompts.Error / Warning / Alert / Info / Intro / Outro | Typed note shortcuts |
Crustum/Prompts.Callout | Structured callout box |
Crustum/Prompts.Table / Crustum/Prompts.Grid | Static table or grid layout |
Crustum/Prompts.Progress | Progress bar (returns Progress or mapped results) |
Crustum/Prompts.Spin | Spinner around a callback |
Crustum/Prompts.Task | Task with live log output |
Crustum/Prompts.Stream | Returns a Laravel\Prompts\Stream instance |
Crustum/Prompts.Title | Set terminal window title |
Crustum/Prompts.Clear | Clear the terminal |
Crustum/Prompts.Notify | Desktop notification (macOS/Linux) |
You may also construct Laravel\Prompts\* classes directly when you need lower-level control. Helpers remain the recommended Cake Console API.
Installation
Install via Composer:
composer require crustum/promptsNOTE
Register the plugin in config/plugins.php, or load it from Application::bootstrap().
bin/cake plugin load Crustum/Prompts// In src/Application.php
public function bootstrap(): void
{
parent::bootstrap();
$this->addPlugin('Crustum/Prompts');
}Plugin bootstrap registers default ConsoleIo fallbacks for interactive and display prompts. Progress, Spinner, and Task use ConsoleIo when shouldFallback() is active and IO is bound. Console helpers call ConsoleIoFallbacks::setIo() for you. If you invoke Laravel Prompts classes directly from a command, bind IO first when fallbacks may run:
use Crustum\Prompts\Cake\ConsoleIoFallbacks;
ConsoleIoFallbacks::setIo($this->io);Available Prompts
Text
The Crustum/Prompts.Text helper prompts the user with the given question, accepts their input, and returns it:
$name = $this->io->helper('Crustum/Prompts.Text')->run([
'label' => 'What is your name?',
]);You may also include placeholder text, a default value, and an informational hint:
$name = $this->io->helper('Crustum/Prompts.Text')->run([
'label' => 'What is your name?',
'placeholder' => 'E.g. Taylor Otwell',
'default' => $user->name ?? '',
'hint' => 'This will be displayed on your profile.',
]);Required Values
If you require a value to be entered, pass the required argument:
$name = $this->io->helper('Crustum/Prompts.Text')->run([
'label' => 'What is your name?',
'required' => true,
]);To customize the validation message, pass a string:
$name = $this->io->helper('Crustum/Prompts.Text')->run([
'label' => 'What is your name?',
'required' => 'Your name is required.',
]);Additional Validation
For additional validation logic, pass a closure to validate:
$name = $this->io->helper('Crustum/Prompts.Text')->run([
'label' => 'What is your name?',
'validate' => fn (string $value) => match (true) {
strlen($value) < 3 => 'The name must be at least 3 characters.',
strlen($value) > 255 => 'The name must not exceed 255 characters.',
default => null,
},
]);The closure receives the entered value and may return an error message, or null if validation passes.
Upstream also accepts Laravel Validator rule arrays when Laravel's validator is available. In a typical CakePHP app, prefer closures.
Textarea
The Crustum/Prompts.Textarea helper prompts for multi-line input and returns it:
$story = $this->io->helper('Crustum/Prompts.Textarea')->run([
'label' => 'Tell me a story.',
]);You may also include placeholder text, a default value, and an informational hint:
$story = $this->io->helper('Crustum/Prompts.Textarea')->run([
'label' => 'Tell me a story.',
'placeholder' => 'This is a story about...',
'hint' => 'This will be displayed on your profile.',
]);Submit the textarea with Ctrl+D.
Required Values
$story = $this->io->helper('Crustum/Prompts.Textarea')->run([
'label' => 'Tell me a story.',
'required' => true,
]);$story = $this->io->helper('Crustum/Prompts.Textarea')->run([
'label' => 'Tell me a story.',
'required' => 'A story is required.',
]);Additional Validation
$story = $this->io->helper('Crustum/Prompts.Textarea')->run([
'label' => 'Tell me a story.',
'validate' => fn (string $value) => match (true) {
strlen($value) < 250 => 'The story must be at least 250 characters.',
strlen($value) > 10000 => 'The story must not exceed 10,000 characters.',
default => null,
},
]);Number
The Crustum/Prompts.Number helper prompts for numeric input. The user may use the up and down arrow keys to change the number:
$number = $this->io->helper('Crustum/Prompts.Number')->run([
'label' => 'How many copies would you like?',
]);You may also include placeholder text, a default value, and an informational hint:
$copies = $this->io->helper('Crustum/Prompts.Number')->run([
'label' => 'How many copies would you like?',
'placeholder' => '5',
'default' => '1',
'hint' => 'This will determine how many copies to create.',
]);Optional min, max, and step arguments constrain and increment the value when supported by upstream.
Required Values
$copies = $this->io->helper('Crustum/Prompts.Number')->run([
'label' => 'How many copies would you like?',
'required' => true,
]);$copies = $this->io->helper('Crustum/Prompts.Number')->run([
'label' => 'How many copies would you like?',
'required' => 'A number of copies is required.',
]);Additional Validation
$copies = $this->io->helper('Crustum/Prompts.Number')->run([
'label' => 'How many copies would you like?',
'validate' => fn (int|string $value) => match (true) {
is_numeric($value) && (int)$value < 1 => 'At least one copy is required.',
is_numeric($value) && (int)$value > 100 => 'You may not create more than 100 copies.',
default => null,
},
]);When the typed value is numeric, upstream returns an int.
Password
The Crustum/Prompts.Password helper is similar to Text, but input is masked as the user types:
$password = $this->io->helper('Crustum/Prompts.Password')->run([
'label' => 'What is your password?',
]);You may also include placeholder text and an informational hint:
$password = $this->io->helper('Crustum/Prompts.Password')->run([
'label' => 'What is your password?',
'placeholder' => 'password',
'hint' => 'Minimum 8 characters.',
]);Required Values
$password = $this->io->helper('Crustum/Prompts.Password')->run([
'label' => 'What is your password?',
'required' => true,
]);$password = $this->io->helper('Crustum/Prompts.Password')->run([
'label' => 'What is your password?',
'required' => 'The password is required.',
]);Additional Validation
$password = $this->io->helper('Crustum/Prompts.Password')->run([
'label' => 'What is your password?',
'validate' => fn (string $value) => match (true) {
strlen($value) < 8 => 'The password must be at least 8 characters.',
default => null,
},
]);Confirm
Use Crustum/Prompts.Confirm for a yes/no confirmation. Users may use the arrow keys or press y / n. The helper returns true or false.
$confirmed = $this->io->helper('Crustum/Prompts.Confirm')->run([
'label' => 'Do you accept the terms?',
]);You may include a default value, custom Yes/No labels, and a hint:
$confirmed = $this->io->helper('Crustum/Prompts.Confirm')->run([
'label' => 'Do you accept the terms?',
'default' => false,
'yes' => 'I accept',
'no' => 'I decline',
'hint' => 'The terms must be accepted to continue.',
]);Requiring "Yes"
$confirmed = $this->io->helper('Crustum/Prompts.Confirm')->run([
'label' => 'Do you accept the terms?',
'required' => true,
]);$confirmed = $this->io->helper('Crustum/Prompts.Confirm')->run([
'label' => 'Do you accept the terms?',
'required' => 'You must accept the terms to continue.',
]);Select
Use Crustum/Prompts.Select when the user must choose from a predefined set of options:
$role = $this->io->helper('Crustum/Prompts.Select')->run([
'label' => 'What role should the user have?',
'options' => ['Member', 'Contributor', 'Owner'],
]);You may specify the default choice and a hint:
$role = $this->io->helper('Crustum/Prompts.Select')->run([
'label' => 'What role should the user have?',
'options' => ['Member', 'Contributor', 'Owner'],
'default' => 'Owner',
'hint' => 'The role may be changed at any time.',
]);Pass an associative array to return the selected key instead of its value:
$role = $this->io->helper('Crustum/Prompts.Select')->run([
'label' => 'What role should the user have?',
'options' => [
'member' => 'Member',
'contributor' => 'Contributor',
'owner' => 'Owner',
],
'default' => 'owner',
]);Up to five options are shown before scrolling. Customize with scroll. Helpers accept Cake\Collection\Collection and convert to arrays:
use Cake\Collection\Collection;
$role = $this->io->helper('Crustum/Prompts.Select')->run([
'label' => 'Which category would you like to assign?',
'options' => new Collection($categoryNamesById),
'scroll' => 10,
]);Secondary Information
The info argument displays additional information about the highlighted option. A closure receives the highlighted value and should return a string or null:
$role = $this->io->helper('Crustum/Prompts.Select')->run([
'label' => 'What role should the user have?',
'options' => [
'member' => 'Member',
'contributor' => 'Contributor',
'owner' => 'Owner',
],
'info' => fn (string $value) => match ($value) {
'member' => 'Can view and comment.',
'contributor' => 'Can view, comment, and edit.',
'owner' => 'Full access to all resources.',
default => null,
},
]);You may also pass a static string:
$role = $this->io->helper('Crustum/Prompts.Select')->run([
'label' => 'What role should the user have?',
'options' => ['Member', 'Contributor', 'Owner'],
'info' => 'The role may be changed at any time.',
]);Additional Validation
By default Select requires a choice (required defaults to true). Pass a validate closure to present an option but prevent it from being selected:
$role = $this->io->helper('Crustum/Prompts.Select')->run([
'label' => 'What role should the user have?',
'options' => [
'member' => 'Member',
'contributor' => 'Contributor',
'owner' => 'Owner',
],
'validate' => fn (string $value) =>
$value === 'owner' && $ownerAlreadyExists
? 'An owner already exists.'
: null,
]);If options is associative, the closure receives the selected key; otherwise it receives the selected value. Return an error message, or null if validation passes.
Multi-select
Use Crustum/Prompts.MultiSelect when the user may select multiple options:
$permissions = $this->io->helper('Crustum/Prompts.MultiSelect')->run([
'label' => 'What permissions should be assigned?',
'options' => ['Read', 'Create', 'Update', 'Delete'],
]);You may specify default choices and a hint:
$permissions = $this->io->helper('Crustum/Prompts.MultiSelect')->run([
'label' => 'What permissions should be assigned?',
'options' => ['Read', 'Create', 'Update', 'Delete'],
'default' => ['Read', 'Create'],
'hint' => 'Permissions may be updated at any time.',
]);Pass an associative array to return selected keys instead of values:
$permissions = $this->io->helper('Crustum/Prompts.MultiSelect')->run([
'label' => 'What permissions should be assigned?',
'options' => [
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
],
'default' => ['read', 'create'],
]);Customize scroll height and pass a Collection:
use Cake\Collection\Collection;
$categories = $this->io->helper('Crustum/Prompts.MultiSelect')->run([
'label' => 'What categories should be assigned?',
'options' => new Collection($categoryNamesById),
'scroll' => 10,
]);Secondary Information
$permissions = $this->io->helper('Crustum/Prompts.MultiSelect')->run([
'label' => 'What permissions should be assigned?',
'options' => [
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
],
'info' => fn (string $value) => match ($value) {
'read' => 'View resources and their properties.',
'create' => 'Create new resources.',
'update' => 'Modify existing resources.',
'delete' => 'Permanently remove resources.',
default => null,
},
]);Requiring a Value
By default the user may select zero or more options. Pass required to enforce one or more:
$categories = $this->io->helper('Crustum/Prompts.MultiSelect')->run([
'label' => 'What categories should be assigned?',
'options' => $categoryNamesById,
'required' => true,
]);$categories = $this->io->helper('Crustum/Prompts.MultiSelect')->run([
'label' => 'What categories should be assigned?',
'options' => $categoryNamesById,
'required' => 'You must select at least one category',
]);Additional Validation
$permissions = $this->io->helper('Crustum/Prompts.MultiSelect')->run([
'label' => 'What permissions should the user have?',
'options' => [
'read' => 'Read',
'create' => 'Create',
'update' => 'Update',
'delete' => 'Delete',
],
'validate' => fn (array $values) => !in_array('read', $values, true)
? 'All users require the read permission.'
: null,
]);If options is associative, the closure receives selected keys; otherwise selected values.
Suggest
Crustum/Prompts.Suggest provides auto-completion for possible choices. The user may still enter any answer:
$name = $this->io->helper('Crustum/Prompts.Suggest')->run([
'label' => 'What is your name?',
'options' => ['Taylor', 'Dayle'],
]);Pass a Closure as options to refresh suggestions as the user types:
use Cake\Collection\Collection;
$name = $this->io->helper('Crustum/Prompts.Suggest')->run([
'label' => 'What is your name?',
'options' => fn ($value) => (new Collection(['Taylor', 'Dayle']))
->filter(fn ($name) => stripos((string)$name, (string)$value) !== false)
->toList(),
]);You may also include placeholder text, a default value, and a hint:
$name = $this->io->helper('Crustum/Prompts.Suggest')->run([
'label' => 'What is your name?',
'options' => ['Taylor', 'Dayle'],
'placeholder' => 'E.g. Taylor',
'default' => $user->name ?? '',
'hint' => 'This will be displayed on your profile.',
]);Secondary Information
$name = $this->io->helper('Crustum/Prompts.Suggest')->run([
'label' => 'What is your name?',
'options' => ['Taylor', 'Dayle'],
'info' => fn (string $value) => match ($value) {
'Taylor' => 'Administrator',
'Dayle' => 'Contributor',
default => null,
},
]);Required Values
$name = $this->io->helper('Crustum/Prompts.Suggest')->run([
'label' => 'What is your name?',
'options' => ['Taylor', 'Dayle'],
'required' => true,
]);$name = $this->io->helper('Crustum/Prompts.Suggest')->run([
'label' => 'What is your name?',
'options' => ['Taylor', 'Dayle'],
'required' => 'Your name is required.',
]);Additional Validation
$name = $this->io->helper('Crustum/Prompts.Suggest')->run([
'label' => 'What is your name?',
'options' => ['Taylor', 'Dayle'],
'validate' => fn (string $value) => match (true) {
strlen($value) < 3 => 'The name must be at least 3 characters.',
strlen($value) > 255 => 'The name must not exceed 255 characters.',
default => null,
},
]);Search
When there are many options, Crustum/Prompts.Search lets the user type a query to filter results before selecting with the arrow keys. The options argument must be a Closure:
$id = $this->io->helper('Crustum/Prompts.Search')->run([
'label' => 'Search for the user that should receive the mail',
'options' => fn (string $value) => strlen($value) > 0
? $this->users->find('list', keyField: 'id', valueField: 'name')
->where(['Users.name LIKE' => "%{$value}%"])
->toArray()
: [],
]);The closure receives the text typed so far and must return an array of options. An associative array returns the selected key; a list returns the selected value.
When filtering a list where you intend to return values, re-index with array_values or Collection toList() so the array does not become associative:
use Cake\Collection\Collection;
$names = new Collection(['Taylor', 'Abigail']);
$selected = $this->io->helper('Crustum/Prompts.Search')->run([
'label' => 'Search for the user that should receive the mail',
'options' => fn (string $value) => $names
->filter(fn ($name) => stripos((string)$name, $value) !== false)
->toList(),
]);You may also include placeholder text and a hint:
$id = $this->io->helper('Crustum/Prompts.Search')->run([
'label' => 'Search for the user that should receive the mail',
'placeholder' => 'E.g. Taylor Otwell',
'options' => fn (string $value) => strlen($value) > 0
? $this->users->find('list', keyField: 'id', valueField: 'name')
->where(['Users.name LIKE' => "%{$value}%"])
->toArray()
: [],
'hint' => 'The user will receive an email immediately.',
]);Customize scroll with scroll:
$id = $this->io->helper('Crustum/Prompts.Search')->run([
'label' => 'Search for the user that should receive the mail',
'options' => fn (string $value) => strlen($value) > 0
? $this->users->find('list', keyField: 'id', valueField: 'name')
->where(['Users.name LIKE' => "%{$value}%"])
->toArray()
: [],
'scroll' => 10,
]);Secondary Information
$id = $this->io->helper('Crustum/Prompts.Search')->run([
'label' => 'Search for the user that should receive the mail',
'options' => fn (string $value) => strlen($value) > 0
? $this->users->find('list', keyField: 'id', valueField: 'name')
->where(['Users.name LIKE' => "%{$value}%"])
->toArray()
: [],
'info' => fn (int $userId) => $this->users->get($userId)->email,
]);Additional Validation
$id = $this->io->helper('Crustum/Prompts.Search')->run([
'label' => 'Search for the user that should receive the mail',
'options' => fn (string $value) => strlen($value) > 0
? $this->users->find('list', keyField: 'id', valueField: 'name')
->where(['Users.name LIKE' => "%{$value}%"])
->toArray()
: [],
'validate' => function (int|string $value) {
$user = $this->users->get($value);
if ($user->opted_out) {
return 'This user has opted-out of receiving mail.';
}
return null;
},
]);Multi-search
Crustum/Prompts.MultiSearch combines search filtering with multi-select (arrow keys + space):
$ids = $this->io->helper('Crustum/Prompts.MultiSearch')->run([
'label' => 'Search for users who should receive the mail',
'options' => fn (string $value) => strlen($value) > 0
? $this->users->find('list', keyField: 'id', valueField: 'name')
->where(['Users.name LIKE' => "%{$value}%"])
->toArray()
: [],
]);Re-index list results when returning values:
use Cake\Collection\Collection;
$names = new Collection(['Taylor', 'Abigail']);
$selected = $this->io->helper('Crustum/Prompts.MultiSearch')->run([
'label' => 'Search for users who should receive the mail',
'options' => fn (string $value) => $names
->filter(fn ($name) => stripos((string)$name, $value) !== false)
->toList(),
]);Placeholder, hint, and scroll:
$ids = $this->io->helper('Crustum/Prompts.MultiSearch')->run([
'label' => 'Search for users who should receive the mail',
'placeholder' => 'E.g. Taylor Otwell',
'options' => fn (string $value) => strlen($value) > 0
? $this->users->find('list', keyField: 'id', valueField: 'name')
->where(['Users.name LIKE' => "%{$value}%"])
->toArray()
: [],
'hint' => 'The user will receive an email immediately.',
'scroll' => 10,
]);Secondary Information
$ids = $this->io->helper('Crustum/Prompts.MultiSearch')->run([
'label' => 'Search for the users that should receive the mail',
'options' => fn (string $value) => strlen($value) > 0
? $this->users->find('list', keyField: 'id', valueField: 'name')
->where(['Users.name LIKE' => "%{$value}%"])
->toArray()
: [],
'info' => fn (int $userId) => $this->users->get($userId)->email,
]);Requiring a Value
$ids = $this->io->helper('Crustum/Prompts.MultiSearch')->run([
'label' => 'Search for the users that should receive the mail',
'options' => fn (string $value) => strlen($value) > 0
? $this->users->find('list', keyField: 'id', valueField: 'name')
->where(['Users.name LIKE' => "%{$value}%"])
->toArray()
: [],
'required' => true,
]);$ids = $this->io->helper('Crustum/Prompts.MultiSearch')->run([
'label' => 'Search for the users that should receive the mail',
'options' => fn (string $value) => strlen($value) > 0
? $this->users->find('list', keyField: 'id', valueField: 'name')
->where(['Users.name LIKE' => "%{$value}%"])
->toArray()
: [],
'required' => 'You must select at least one user.',
]);Additional Validation
$ids = $this->io->helper('Crustum/Prompts.MultiSearch')->run([
'label' => 'Search for the users that should receive the mail',
'options' => fn (string $value) => strlen($value) > 0
? $this->users->find('list', keyField: 'id', valueField: 'name')
->where(['Users.name LIKE' => "%{$value}%"])
->toArray()
: [],
'validate' => function (array $values) {
$optedOut = $this->users->find()
->where(['Users.id IN' => $values, 'Users.opted_out' => true])
->all();
if (!$optedOut->isEmpty()) {
return implode(', ', $optedOut->extract('name')->toList()) . ' have opted out.';
}
return null;
},
]);Pause
Crustum/Prompts.Pause displays text and waits for Enter / Return:
$this->io->helper('Crustum/Prompts.Pause')->run([
'message' => 'Press ENTER to continue.',
]);Autocomplete
Crustum/Prompts.AutoComplete provides inline ghost-text completion. Matching suggestions can be accepted with Tab or the right arrow key:
$name = $this->io->helper('Crustum/Prompts.AutoComplete')->run([
'label' => 'What is your name?',
'options' => ['Taylor', 'Dayle', 'Jess', 'Nuno', 'Tim'],
]);Placeholder, default, and hint:
$name = $this->io->helper('Crustum/Prompts.AutoComplete')->run([
'label' => 'What is your name?',
'options' => ['Taylor', 'Dayle', 'Jess', 'Nuno', 'Tim'],
'placeholder' => 'E.g. Taylor',
'default' => $user->name ?? '',
'hint' => 'Use tab to accept, up/down to cycle.',
]);Dynamic Options
Pass a Closure to generate options from the current input:
use Cake\Collection\Collection;
$file = $this->io->helper('Crustum/Prompts.AutoComplete')->run([
'label' => 'Which file?',
'options' => fn (string $value) => (new Collection($files))
->filter(fn ($file) => str_starts_with(strtolower((string)$file), strtolower($value)))
->toList(),
]);Required Values
$name = $this->io->helper('Crustum/Prompts.AutoComplete')->run([
'label' => 'What is your name?',
'options' => ['Taylor', 'Dayle', 'Jess', 'Nuno', 'Tim'],
'required' => true,
]);$name = $this->io->helper('Crustum/Prompts.AutoComplete')->run([
'label' => 'What is your name?',
'options' => ['Taylor', 'Dayle', 'Jess', 'Nuno', 'Tim'],
'required' => 'Your name is required.',
]);Additional Validation
$name = $this->io->helper('Crustum/Prompts.AutoComplete')->run([
'label' => 'What is your name?',
'options' => ['Taylor', 'Dayle', 'Jess', 'Nuno', 'Tim'],
'validate' => fn (string $value) => match (true) {
strlen($value) < 3 => 'The name must be at least 3 characters.',
strlen($value) > 255 => 'The name must not exceed 255 characters.',
default => null,
},
]);Transforming Input Before Validation
Many helpers accept a transform Closure that runs before validation — for example to trim whitespace:
$name = $this->io->helper('Crustum/Prompts.Text')->run([
'label' => 'What is your name?',
'transform' => fn (string $value) => trim($value),
'validate' => fn (string $value) => match (true) {
strlen($value) < 3 => 'The name must be at least 3 characters.',
strlen($value) > 255 => 'The name must not exceed 255 characters.',
default => null,
},
]);Forms
Crustum/Prompts.Form returns a Laravel\Prompts\FormBuilder so you can group prompts. The user can return to previous steps with Ctrl+U:
$responses = $this->io->helper('Crustum/Prompts.Form')->run([])
->text('What is your name?', required: true)
->password('What is your password?', validate: fn (string $value) =>
strlen($value) < 8 ? 'Minimum 8 characters.' : null
)
->confirm('Do you accept the terms?')
->submit();submit() returns a numerically indexed array of responses. Pass name to access responses by key:
$responses = $this->io->helper('Crustum/Prompts.Form')->run([])
->text('What is your name?', required: true, name: 'name')
->password(
label: 'What is your password?',
validate: fn (string $value) =>
strlen($value) < 8 ? 'Minimum 8 characters.' : null,
name: 'password',
)
->confirm('Do you accept the terms?')
->submit();
$user = $this->users->newEntity([
'name' => $responses['name'],
'password' => $responses['password'],
]);
$this->users->saveOrFail($user);For granular control, use add. The callback receives previous responses:
$responses = $this->io->helper('Crustum/Prompts.Form')->run([])
->text('What is your name?', required: true, name: 'name')
->add(function ($responses) {
return $this->io->helper('Crustum/Prompts.Text')->run([
'label' => "How old are you, {$responses['name']}?",
]);
}, name: 'age')
->submit();
$this->io->helper('Crustum/Prompts.Outro')->run([
'message' => "Your name is {$responses['name']} and you are {$responses['age']} years old.",
]);You may also call FormBuilder prompt methods that construct upstream prompts directly (->text(), ->select(), …); those still run through laravel/prompts.
Informational Messages
Use note helpers to display informational messages:
$this->io->helper('Crustum/Prompts.Info')->run([
'message' => 'Package installed successfully.',
]);Also available: Crustum/Prompts.Note (with optional type), Warning, Error, Alert, Intro, and Outro.
$this->io->helper('Crustum/Prompts.Note')->run([
'message' => 'Heads up.',
'type' => 'warning',
]);Callouts
Crustum/Prompts.Callout displays a boxed message with a label and content:
$this->io->helper('Crustum/Prompts.Callout')->run([
'label' => 'Environment Configured',
'content' => 'Your application is running in production mode with 4 workers.',
]);Pass warning or error as type to change the visual style:
$this->io->helper('Crustum/Prompts.Callout')->run([
'label' => 'Deprecation Notice',
'content' => 'The `--prefer-stable` flag will be removed in v4.0. Use `--stability=stable` instead.',
'type' => 'warning',
]);
$this->io->helper('Crustum/Prompts.Callout')->run([
'label' => 'Database Connection Failed',
'content' => 'Could not connect to MySQL on 127.0.0.1:3306.',
'type' => 'error',
]);The info argument adds a footer line:
$this->io->helper('Crustum/Prompts.Callout')->run([
'label' => 'Deployment Summary',
'content' => 'Your application was deployed to production.',
'info' => 'deploy-id: d4f8a2c',
]);Rich Content
Instead of a string, pass an array of strings and elements. Use Laravel\Prompts\Elements\Element factory methods for headings, lists, key/value pairs, and links:
use Laravel\Prompts\Elements\Element;
$this->io->helper('Crustum/Prompts.Callout')->run([
'label' => 'Deployment Summary',
'content' => [
'Your application was deployed to production at 2024-03-15 14:32 UTC.',
Element::heading('What Changed'),
Element::bulletedList([
'Migrated 3 pending database migrations',
'Cleared and rebuilt route cache',
'Restarted 4 queue workers',
]),
Element::heading('Next Steps'),
Element::numberedList([
'Verify the health check endpoint at /up',
'Monitor error rates for the next 15 minutes',
'Confirm background jobs are processing',
]),
],
]);$this->io->helper('Crustum/Prompts.Callout')->run([
'label' => 'Database Connection Failed',
'content' => [
'Could not connect to the database server.',
Element::keyValueList([
'Host' => '127.0.0.1',
'Port' => '3306',
'Database' => 'forge',
'Status' => 'Connection refused',
]),
],
'type' => 'error',
]);Element::link creates a clickable hyperlink in terminals that support OSC 8:
$this->io->helper('Crustum/Prompts.Callout')->run([
'label' => 'Server Health Check',
'content' => [
'Multiple services are reporting degraded performance.',
Element::heading('Affected Services'),
'Look here: ' . Element::link('https://example.com/health', 'Health Dashboard'),
Element::link('https://example.com/health'),
],
]);If no label is provided, the URL itself is shown as the link text.
Tables
Crustum/Prompts.Table displays rows and columns:
$this->io->helper('Crustum/Prompts.Table')->run([
'headers' => ['Name', 'Email'],
'rows' => [
['Taylor Otwell', 'taylor@example.com'],
['Jason Beggs', 'jason@example.com'],
],
]);Crustum/Prompts.Grid lays out a list of items:
$this->io->helper('Crustum/Prompts.Grid')->run([
'items' => ['Alpha', 'Bravo', 'Charlie', 'Delta'],
'maxWidth' => 80,
]);Crustum/Prompts.DataTable is an interactive searchable table that returns the selected row:
$row = $this->io->helper('Crustum/Prompts.DataTable')->run([
'label' => 'Choose a user',
'headers' => ['Name', 'Email'],
'rows' => [
['Taylor Otwell', 'taylor@example.com'],
['Jason Beggs', 'jason@example.com'],
],
'scroll' => 10,
]);Spin
Crustum/Prompts.Spin shows a spinner while a callback runs, then returns the callback result:
$response = $this->io->helper('Crustum/Prompts.Spin')->run([
'callback' => fn () => file_get_contents('https://example.com'),
'message' => 'Fetching response...',
]);WARNING
Upstream spinner animation requires the PCNTL extension. Without it, a static spinner is shown. On Windows / fallback mode, the Cake helper runs the callback with ConsoleIo messaging instead.
Progress Bars
Crustum/Prompts.Progress shows progress for long-running work. With a callback, it maps over steps and returns an array of callback results:
$users = $this->io->helper('Crustum/Prompts.Progress')->run([
'label' => 'Updating users',
'steps' => $userList,
'callback' => fn ($user) => $this->performTask($user),
]);The callback may also accept the Laravel\Prompts\Progress instance to update label and hint per iteration:
$users = $this->io->helper('Crustum/Prompts.Progress')->run([
'label' => 'Updating users',
'steps' => $userList,
'callback' => function ($user, $progress) {
$progress
->label("Updating {$user->name}")
->hint("Created on {$user->created_at}");
return $this->performTask($user);
},
'hint' => 'This may take some time.',
]);Without a callback, the helper returns a Progress instance for manual control:
$progress = $this->io->helper('Crustum/Prompts.Progress')->run([
'label' => 'Updating users',
'steps' => 10,
]);
$progress->start();
foreach ($userList as $user) {
$this->performTask($user);
$progress->advance();
}
$progress->finish();Task
Crustum/Prompts.Task shows a labeled task with a spinner and scrolling live output while a callback runs:
$this->io->helper('Crustum/Prompts.Task')->run([
'label' => 'Installing dependencies',
'callback' => function ($logger) {
// Long-running process...
},
]);The callback receives a Laravel\Prompts\Support\Logger for log lines, status messages, and streamed text.
WARNING
Animation requires PCNTL. Without it, a static task UI is shown. Cake fallbacks run when shouldFallback() is active.
Logging Lines
$this->io->helper('Crustum/Prompts.Task')->run([
'label' => 'Installing dependencies',
'callback' => function ($logger) {
$logger->line('Resolving packages...');
$logger->line('Downloading laravel/framework');
},
]);Status Messages
Use success, warning, and error for stable status lines above the scrolling log:
$this->io->helper('Crustum/Prompts.Task')->run([
'label' => 'Deploying application',
'callback' => function ($logger) {
$logger->line('Pulling latest changes...');
$logger->success('Changes pulled!');
$logger->line('Running migrations...');
$logger->warning('No new migrations to run.');
$logger->line('Clearing cache...');
$logger->success('Cache cleared!');
},
]);Updating the Label
$this->io->helper('Crustum/Prompts.Task')->run([
'label' => 'Starting deployment...',
'callback' => function ($logger) {
$logger->label('Pulling latest changes...');
$logger->label('Running migrations...');
$logger->label('Clearing cache...');
},
]);Displaying a Sub-Label
subLabel shows a dim line under the main label. Pass an empty string to clear it. You may also set an initial value via the helper argument:
$this->io->helper('Crustum/Prompts.Task')->run([
'label' => 'Deploying',
'subLabel' => 'Preparing...',
'callback' => function ($logger) {
$logger->subLabel('Building assets...');
$logger->subLabel('Running migrations...');
$logger->subLabel('');
},
]);Streaming Text
For incremental output, use partial then commitPartial:
$this->io->helper('Crustum/Prompts.Task')->run([
'label' => 'Generating response...',
'callback' => function ($logger) use ($words) {
foreach ($words as $word) {
$logger->partial($word . ' ');
}
$logger->commitPartial();
},
]);Customizing the Output Limit
Default visible log lines is 10. Override with limit:
$this->io->helper('Crustum/Prompts.Task')->run([
'label' => 'Installing dependencies',
'callback' => function ($logger) {
// ...
},
'limit' => 20,
]);Keeping the Summary
By default task output is erased when finished. Pass keepSummary to retain status messages:
$this->io->helper('Crustum/Prompts.Task')->run([
'label' => 'Deploying',
'callback' => function ($logger) {
$logger->success('Assets built');
$logger->success('Migrations complete');
},
'keepSummary' => true,
]);Stream
Crustum/Prompts.Stream returns a Laravel\Prompts\Stream for incremental terminal text (for example AI output):
$stream = $this->io->helper('Crustum/Prompts.Stream')->run([]);
foreach ($words as $word) {
$stream->append($word . ' ');
usleep(25_000);
}
$stream->close();append adds text with a gradual fade-in. Call close when finished to finalize output and restore the cursor.
Terminal Title
Crustum/Prompts.Title updates the terminal window or tab title:
$this->io->helper('Crustum/Prompts.Title')->run([
'title' => 'Installing Dependencies',
]);Reset with an empty string:
$this->io->helper('Crustum/Prompts.Title')->run([
'title' => '',
]);Clearing the Terminal
$this->io->helper('Crustum/Prompts.Clear')->run([]);Terminal Considerations
Terminal Width
If a label, option, or validation message exceeds the terminal column count, it is truncated. Prefer shorter strings on narrow terminals. A typically safe maximum is 74 characters for an 80-column terminal.
Terminal Height
For prompts that accept scroll, the configured value is reduced automatically to fit the terminal height, including space for a validation message.
Unsupported Environments and Fallbacks
Laravel Prompts supports macOS, Linux, and Windows with WSL. Native Windows PHP cannot drive the interactive TTY UI.
This plugin registers CakePHP ConsoleIo fallbacks via Crustum\Prompts\Cake\ConsoleIoFallbacks. Plugin bootstrap calls registerDefaults(). Helpers call setIo() so fallbacks have a bound IO.
ConsoleIoFallbacks::enableEnvironmentFallbacks() enables upstream Prompt::fallbackWhen() on Windows and when ConsoleIo is non-interactive.
Fallback Conditions
To customize when fallbacks run (for example in tests), use Laravel's sticky API:
use Laravel\Prompts\Prompt;
Prompt::fallbackWhen(true);Clear and re-apply environment defaults between tests with Crustum\Prompts\Testing\PromptState::reset().
Fallback Behavior
Defaults are registered for interactive prompts (Text through DataTable), display prompts (Note through Notify, Table, Grid, Clear, Title), and progress-style helpers. Progress, Spinner, and Task honor Cake fallbacks from the helper when shouldFallback() is true, because upstream map() / spin() / run() do not always delegate to registered fallbacks automatically.
You may still override a single prompt class with fallbackUsing if you need custom behavior:
use Laravel\Prompts\TextPrompt;
TextPrompt::fallbackUsing(function (TextPrompt $prompt) {
// Custom fallback; return an appropriate value for the prompt.
});Prefer the packaged ConsoleIo fallbacks for Cake commands.