Artisan, Tinker and Prompts
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Laravel 13.x (13.34.0) and Laravel Prompts 0.3 on PHP 8.3 to 8.5, checked on 2026-10-05; every command and output was run in the Bookshelf application. Artisan is built on Symfony Console, so the concepts on the PHP Reference’s Command-Line PHP page (arguments, options, exit codes, standard streams, signals) apply directly.
The built-in commands
php artisan list shows every command, grouped by namespace; php artisan help <command> (or --help) shows its
arguments and options. The groups used most often:
| Group | Examples |
|---|---|
|
Generators: |
|
|
|
Inspect and cache: |
|
|
|
|
Others |
|
Source: Laravel — Artisan Console
Generators fill files from stubs. php artisan stub:publish copies the stubs to stubs/ in the project, where
they can be edited (for example to add declare(strict_types=1); or a licence header to every generated class).
Writing a command
php artisan make:command ImportBooks creates a class in app/Console/Commands, discovered automatically. The
signature declares the name, arguments and options in one string; handle() does the work and returns an exit code.
Bookshelf’s CSV importer:
<?php
// app/Console/Commands/ImportBooks.php
namespace App\Console\Commands;
use App\Models\Author;
use App\Models\Book;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Isolatable;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\table;
class ImportBooks extends Command implements Isolatable, PromptsForMissingInput
{
protected $signature = 'books:import
{file : Path to a CSV file with title,author,year rows}
{--dry-run : Show what would be imported without saving}
{--limit=0 : Import at most this many rows (0 = all)}';
protected $description = 'Import books from a CSV file';
public function handle(): int
{
$path = $this->argument('file');
if (! is_readable($path)) {
$this->error("Cannot read $path");
return self::FAILURE; // exit code 1
}
$rows = array_map('str_getcsv', file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES));
if ($limit = (int) $this->option('limit')) {
$rows = array_slice($rows, 0, $limit);
}
table(headers: ['Title', 'Author', 'Year'], rows: $rows);
if ($this->option('dry-run') || ! confirm('Import '.count($rows).' books?', default: true)) {
$this->info('Nothing imported.');
return self::SUCCESS;
}
$this->withProgressBar($rows, function (array $row) {
[$title, $author, $year] = $row + [2 => null];
Book::create([
'author_id' => Author::firstOrCreate(['name' => $author])->id,
'title' => $title,
'isbn' => 'import-'.md5($title.$author),
'published_year' => $year ?: null,
]);
});
$this->newLine();
$this->info('Imported '.count($rows).' books.');
return self::SUCCESS;
}
/** Questions to ask when a required argument is missing (PromptsForMissingInput). */
protected function promptForMissingArgumentsUsing(): array
{
return ['file' => ['Which CSV file should be imported?', 'storage/app/import/books.csv']];
}
}
The signature syntax: {file} is a required argument, {file?} optional, {file=default} with a default,
{files*} an array; {--dry-run} is a boolean option, {--limit=} an option with a value, {--limit=0} with a
default, {--L|limit=} with a shortcut, {--tag=*} an array option; text after ` : ` is the help description.
Laravel generates the help page from it:
$ php artisan books:import --help
Description:
Import books from a CSV file
Usage:
books:import [options] [--] <file>
Arguments:
file Path to a CSV file with title,author,year rows
Options:
--dry-run Show what would be imported without saving
--limit[=LIMIT] Import at most this many rows (0 = all) [default: "0"]
--isolated[=ISOLATED] Do not run the command if another instance of the command is already running [default: false]
...
$ php artisan books:import storage/app/import/books.csv --dry-run
┌─────────────────────┬───────────────────┬──────┐
│ Title │ Author │ Year │
├─────────────────────┼───────────────────┼──────┤
│ Wild Seed │ Octavia E. Butler │ 1980 │
│ The Lathe of Heaven │ Ursula K. Le Guin │ 1971 │
│ The Obelisk Gate │ N. K. Jemisin │ 2016 │
└─────────────────────┴───────────────────┴──────┘
Nothing imported.
$ php artisan books:import storage/app/import/books.csv --limit=2 --no-interaction
...
2/2 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
Imported 2 books.
$ php artisan books:import nope.csv -n; echo "exit $?"
Cannot read nope.csv
exit 1
Source: Laravel — Command I/O and Exit Codes
What the run shows:
-
Exit codes matter for scripts, cron and CI:
self::SUCCESS(0),self::FAILURE(1),self::INVALID(2). An uncaught exception also exits with 1. -
--no-interaction(-n) answers every prompt with its default (confirm(default: true)here), so the same command works in a deployment script. -
Isolatableadds the--isolatedoption, which takes a cache lock so that only one instance runs at a time; a second instance exits without running. -
PromptsForMissingInput: runningphp artisan books:importwithout the file asks for it instead of failing with "Not enough arguments".
Output helpers: info(), comment(), warn(), error(), line(), newLine(), table() and
withProgressBar(), or the Prompts functions below. Long-running commands can react to signals with
$this->trap(SIGTERM, fn () => $this->shouldKeepRunning = false) to finish the current item before exiting.
Closure commands and calling commands from code
Small commands can be closures in routes/console.php, which is also where the schedule lives:
<?php
// routes/console.php
use App\Models\Book;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('books:count', function () {
$this->info(Book::count().' books in the catalogue');
})->purpose('Count the books');
Schedule::command('books:import storage/app/import/books.csv --no-interaction')->dailyAt('03:00');
Source: Laravel — Closure Commands
Artisan::call('books:import', ['file' => …, '--dry-run' => true]) runs a command from code and returns its exit code
(int(0) for the dry run in the test application), and Artisan::output() returns what it printed; Artisan::queue() runs it on a queue worker instead. From
inside another command, $this->call() and $this->callSilently() do the same.
Tinker
php artisan tinker is a REPL (based on PsySH) with the application booted: models, facades and helpers are all
available, which makes it the quickest way to try a query or inspect data. --execute runs one snippet and exits,
which is how many examples in this section were produced:
$ php artisan tinker # interactive: type PHP, see the result, exit to quit
$ php artisan tinker --execute='echo App\Models\Book::whereRelation("author", "name", "Octavia E. Butler")->orderBy("published_year")->pluck("title")->toJson();'
["Kindred","Wild Seed","Parable of the Sower"]
$ php artisan books:count # the closure command above
25 books in the catalogue
$ php artisan schedule:list
0 3 * * * php artisan books:import storage/app/import/books.csv --no-interaction Next Due: 7 hours from now
Source: Laravel — Tinker (REPL)
Tinker runs with the full permissions of the application, against whatever database .env points to. Use it in
production only with great care (prefer read-only queries), and never paste untrusted code into it.
Laravel Prompts
Laravel Prompts draws interactive forms in the terminal: text, textarea, number, password, confirm,
select, multiselect, suggest, search, multisearch, pause and autocomplete, plus output helpers (info,
warning, error, alert, note, intro, outro, table, callout), spin, progress and task. Each input
prompt supports a placeholder, a default, a hint, required, and a validate closure that returns an error message or
null. form() chains several prompts and returns all answers. It works in any PHP command-line application, not
only in Artisan.
The form below ran with simulated keystrokes (Prompt::fake(), as in tests): the title typed as Dawn, the rating
moved down two places, and the confirmation accepted:
<?php
require __DIR__ . '/vendor/autoload.php';
use Laravel\Prompts\Key;
use Laravel\Prompts\Prompt;
use function Laravel\Prompts\form;
// Simulated keystrokes, as in a test: type "Dawn", Enter; arrow down twice, Enter; Enter (accept default)
Prompt::fake(['D', 'a', 'w', 'n', Key::ENTER, Key::DOWN, Key::DOWN, Key::ENTER, Key::ENTER]);
$review = form()
->text('Book title', required: true, validate: fn (string $v) => mb_strlen($v) < 2 ? 'Too short.' : null, name: 'title')
->select('Rating', options: [5 => '★★★★★', 4 => '★★★★', 3 => '★★★'], name: 'rating')
->confirm('Publish now?', default: true, name: 'publish')
->submit();
var_dump($review);
// array(3) { ["title"]=> string(4) "Dawn" ["rating"]=> int(3) ["publish"]=> bool(true) }
Source: Laravel — Prompts and Prompts: Forms
Prompts supports macOS, Linux and Windows under WSL. In unsupported environments (native Windows PHP), Laravel
automatically falls back to Symfony Console’s question helper for each prompt. When input is not interactive (cron,
CI, --no-interaction), prompts return their defaults, and a required prompt without a default fails.
spin() and task() animate only when the pcntl extension is available.
Testing commands
Tests run a command with $this->artisan() and describe the conversation: the questions, the answers and the
output. The same assertions work for Symfony-style questions and for Prompts:
<?php
// tests/Feature/ImportBooksTest.php
use App\Models\Book;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('it asks for the missing file and confirms before importing', function () {
$this->artisan('books:import')
->expectsQuestion('Which CSV file should be imported?', 'storage/app/import/books.csv')
->expectsPromptsTable(headers: ['Title', 'Author', 'Year'], rows: [
['Wild Seed', 'Octavia E. Butler', '1980'],
['The Lathe of Heaven', 'Ursula K. Le Guin', '1971'],
['The Obelisk Gate', 'N. K. Jemisin', '2016'],
])
->expectsConfirmation('Import 3 books?', 'yes')
->expectsOutputToContain('Imported 3 books.')
->assertSuccessful();
expect(Book::count())->toBe(3);
});
test('answering no imports nothing', function () {
$this->artisan('books:import', ['file' => 'storage/app/import/books.csv'])
->expectsConfirmation('Import 3 books?', 'no')
->expectsOutput('Nothing imported.')
->assertExitCode(0);
expect(Book::count())->toBe(0);
});
$ php artisan test --filter=ImportBooksTest
PASS Tests\Feature\ImportBooksTest
✓ it asks for the missing file and confirms before importing 0.22s
✓ answering no imports nothing 0.01s
Tests: 2 passed (10 assertions)
Source: Laravel — Console Tests and Prompts: Testing
A detail found while writing the test: expectsTable() matches tables printed with the command’s own table()
method (Symfony’s renderer), while a table drawn with the Prompts table() function needs expectsPromptsTable();
using the wrong one fails with Output "+---------------------+…" was not printed.
Where older books and tutorials are outdated
-
Registering commands in
app/Console/Kernel.php(Stauffer ch. 8) is unnecessary: commands inapp/Console/Commandsare discovered, and closure commands and the schedule live inroutes/console.php(Laravel 11). -
$this->ask(),$this->choice()and$this->secret()still work, but Laravel Prompts (Laravel 10) gives validation, search, multi-select and forms with a much better terminal experience. -
exit()in commands: returnself::SUCCESSorself::FAILUREfromhandle()instead, so thatArtisan::call()and tests receive the code. -
Running two copies of a long job from cron needed hand-written lock files;
Isolatable(Laravel 9.x) and the scheduler’swithoutOverlapping()handle it.