Si alguna vez has creado comandos Artisan en Laravel, sabrás que la interacción con el usuario solía ser bastante básica. Con Laravel Prompts, eso cambió por completo. Este paquete transforma tus comandos de consola en experiencias interactivas con interfaces similares a las de un navegador web.
En esta guía vamos a construir ejemplos reales y funcionales que podrás usar en tus proyectos desde hoy mismo.
Qué es Laravel Prompts
Laravel Prompts es un paquete PHP de primera parte desarrollado por el equipo de Laravel. Añade formularios hermosos e intuitivos a tus aplicaciones de línea de comandos con características como texto placeholder, validación en tiempo real, navegación entre campos y sugerencias automáticas.
Fue presentado por Jess Archer en Laracon US 2023 y desde entonces se ha convertido en el estándar para crear comandos Artisan profesionales.
Compatibilidad y Requisitos
Antes de comenzar, es importante conocer los requisitos y limitaciones del paquete.
Sistemas Operativos Soportados
Laravel Prompts funciona en:
macOS
Linux
Windows con WSL (Windows Subsystem for Linux)
Limitación en Windows Nativo
Debido a limitaciones en la versión de PHP para Windows, actualmente no es posible usar Laravel Prompts en Windows fuera de WSL. El paquete hace fallback automático al Question Helper de Symfony en entornos no soportados.
Si usas Laravel, los fallbacks ya están configurados y se activan automáticamente. Si usas el paquete fuera de Laravel, puedes configurar el fallback manualmente:
<?php
use Laravel\Prompts\Prompt;
Prompt::fallbackWhen(
fn () => PHP_OS_FAMILY === 'Windows' && !getenv('WSL_DISTRO_NAME')
);Versiones Requeridas
PHP 8.1 o superior
Laravel 10.17 o superior (viene incluido por defecto)
Para proyectos sin Laravel: instalación vía Composer
Instalación
En Laravel 10.17+ ya viene incluido, no necesitas hacer nada. Para proyectos PHP sin Laravel:
composer require laravel/promptsNo hay archivos de configuración ni service providers que publicar.
Tipos de Prompts Disponibles
Vamos a ver cada tipo de prompt con ejemplos prácticos.
Text: Entrada de Texto
El prompt más básico pero con muchas opciones:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use function Laravel\Prompts\text;
class CreateProjectCommand extends Command
{
protected $signature = 'project:create';
protected $description = 'Crea un nuevo proyecto';
public function handle(): int
{
$name = text(
label: '¿Cuál es el nombre del proyecto?',
placeholder: 'mi-proyecto-laravel',
required: 'El nombre del proyecto es obligatorio.',
validate: fn (string $value) => match (true) {
strlen($value) < 3 => 'El nombre debe tener al menos 3 caracteres.',
!preg_match('/^[a-z0-9\-]+$/', $value) => 'Solo minúsculas, números y guiones.',
default => null,
},
hint: 'Usa solo letras minúsculas, números y guiones.',
);
$this->info("Proyecto '{$name}' configurado.");
return self::SUCCESS;
}
}Textarea: Texto Multilínea
Para descripciones o contenido extenso:
<?php
use function Laravel\Prompts\textarea;
$content = textarea(
label: 'Descripción del proyecto:',
placeholder: 'Describe tu proyecto...',
required: true,
validate: fn (string $value) => strlen($value) > 5000
? 'Máximo 5000 caracteres.'
: null,
hint: 'Enter para nuevas líneas.',
);Password: Entrada Segura
Para contraseñas y datos sensibles:
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use function Laravel\Prompts\password;
use function Laravel\Prompts\text;
class ResetPasswordCommand extends Command
{
protected $signature = 'user:reset-password';
protected $description = 'Restablece la contraseña de un usuario';
public function handle(): int
{
$email = text(
label: 'Email del usuario:',
required: true,
validate: fn (string $value) => filter_var($value, FILTER_VALIDATE_EMAIL)
? null
: 'Introduce un email válido.',
);
$user = User::where('email', $email)->first();
if (!$user) {
$this->error('Usuario no encontrado.');
return self::FAILURE;
}
$password = password(
label: 'Nueva contraseña:',
required: 'La contraseña es obligatoria.',
validate: fn (string $value) => match (true) {
strlen($value) < 8 => 'Mínimo 8 caracteres.',
!preg_match('/[A-Z]/', $value) => 'Debe incluir una mayúscula.',
!preg_match('/[0-9]/', $value) => 'Debe incluir un número.',
default => null,
},
hint: 'Mínimo 8 caracteres, una mayúscula y un número.',
);
$confirmation = password(
label: 'Confirma la contraseña:',
required: true,
validate: fn (string $value) => $value !== $password
? 'Las contraseñas no coinciden.'
: null,
);
$user->update(['password' => Hash::make($password)]);
$this->info("Contraseña actualizada para {$user->name}.");
return self::SUCCESS;
}
}Confirm: Confirmación Sí/No
Para acciones que requieren confirmación:
<?php
use function Laravel\Prompts\confirm;
$confirmed = confirm(
label: '¿Estás seguro de eliminar este registro?',
default: false,
yes: 'Sí, eliminar',
no: 'No, cancelar',
hint: 'Esta acción no se puede deshacer.',
);
if (!$confirmed) {
$this->info('Operación cancelada.');
return self::SUCCESS;
}Select: Selección Única
Para elegir una opción de una lista:
<?php
use function Laravel\Prompts\select;
$role = select(
label: 'Selecciona el rol del usuario:',
options: [
'viewer' => 'Viewer - Solo lectura',
'editor' => 'Editor - Puede editar contenido',
'admin' => 'Admin - Acceso completo',
],
default: 'viewer',
scroll: 5,
hint: 'Usa las flechas para navegar.',
);También puedes usar opciones dinámicas desde la base de datos:
<?php
use App\Models\Category;
use function Laravel\Prompts\select;
$categoryId = select(
label: '¿En qué categoría va el producto?',
options: Category::pluck('name', 'id')->all(),
scroll: 10,
);Multiselect: Selección Múltiple
Cuando el usuario necesita elegir varias opciones:
<?php
use function Laravel\Prompts\multiselect;
$permissions = multiselect(
label: '¿Qué permisos asignar?',
options: [
'read' => 'Lectura',
'create' => 'Crear',
'update' => 'Actualizar',
'delete' => 'Eliminar',
],
default: ['read'],
required: 'Selecciona al menos un permiso.',
scroll: 10,
hint: 'Espacio para seleccionar, Enter para confirmar.',
);Search: Búsqueda Dinámica
Para listas grandes donde el usuario necesita buscar:
<?php
namespace App\Console\Commands;
use App\Models\Product;
use Illuminate\Console\Command;
use function Laravel\Prompts\search;
use function Laravel\Prompts\text;
class UpdateStockCommand extends Command
{
protected $signature = 'stock:update';
protected $description = 'Actualiza el stock de un producto';
public function handle(): int
{
$productId = search(
label: 'Busca el producto:',
options: function (string $value) {
if (strlen($value) < 2) {
return [];
}
return Product::query()
->where('name', 'like', "%{$value}%")
->orWhere('sku', 'like', "%{$value}%")
->limit(10)
->get()
->mapWithKeys(fn (Product $p) => [
$p->id => "{$p->sku} - {$p->name} (Stock: {$p->stock})",
])
->all();
},
placeholder: 'Nombre o SKU del producto...',
hint: 'Mínimo 2 caracteres.',
);
$product = Product::findOrFail($productId);
$newStock = text(
label: 'Nuevo stock:',
default: (string) $product->stock,
required: true,
validate: fn (string $value) => !is_numeric($value) || $value < 0
? 'Introduce un número válido.'
: null,
);
$product->update(['stock' => (int) $newStock]);
$this->info("Stock actualizado a {$newStock} unidades.");
return self::SUCCESS;
}
}Multisearch: Búsqueda con Selección Múltiple
Combina búsqueda dinámica con selección múltiple:
<?php
use App\Models\Tag;
use function Laravel\Prompts\multisearch;
$tagIds = multisearch(
label: 'Selecciona las etiquetas:',
options: function (string $value) {
return Tag::where('name', 'like', "%{$value}%")
->limit(15)
->pluck('name', 'id')
->all();
},
placeholder: 'Busca etiquetas...',
hint: 'Puedes seleccionar múltiples.',
);Suggest: Autocompletado
Para campos con sugerencias pero que permiten valores personalizados:
<?php
use function Laravel\Prompts\suggest;
$country = suggest(
label: 'País:',
options: fn (string $value) => collect([
'España', 'México', 'Argentina', 'Colombia', 'Chile',
'Perú', 'Ecuador', 'Venezuela', 'Uruguay', 'Paraguay',
])
->filter(fn ($c) => str_contains(strtolower($c), strtolower($value)))
->values()
->all(),
placeholder: 'Escribe o selecciona...',
required: true,
);Indicadores de Progreso
Spin: Animación de Carga
Para operaciones que tardan pero no tienen progreso medible:
<?php
use Illuminate\Support\Facades\Http;
use function Laravel\Prompts\spin;
$response = spin(
callback: fn () => Http::timeout(30)->get('https://api.ejemplo.com/data'),
message: 'Conectando con la API...',
);
if ($response->successful()) {
$this->info('Datos recibidos correctamente.');
}La función spin requiere la extensión pcntl de PHP para la animación. Si no está disponible, muestra una versión estática.
Progress: Barra de Progreso
Para operaciones con progreso medible:
<?php
namespace App\Console\Commands;
use App\Models\User;
use App\Notifications\WeeklyReport;
use Illuminate\Console\Command;
use function Laravel\Prompts\progress;
class SendReportsCommand extends Command
{
protected $signature = 'reports:send';
protected $description = 'Envía reportes a todos los usuarios';
public function handle(): int
{
$users = User::where('receive_reports', true)->get();
if ($users->isEmpty()) {
$this->warn('No hay usuarios suscritos.');
return self::SUCCESS;
}
$results = progress(
label: 'Enviando reportes',
steps: $users,
callback: function (User $user, $progress) {
$progress
->label("Enviando a {$user->name}")
->hint($user->email);
try {
$user->notify(new WeeklyReport());
return ['user' => $user->email, 'status' => 'sent'];
} catch (\Exception $e) {
return ['user' => $user->email, 'status' => 'failed'];
}
},
hint: 'Esto puede tardar...',
);
$sent = collect($results)->where('status', 'sent')->count();
$this->info("Enviados: {$sent} de {$users->count()}");
return self::SUCCESS;
}
}Progress Manual
Para control total sobre la barra:
<?php
use function Laravel\Prompts\progress;
$progress = progress(label: 'Importando datos', steps: 100);
$progress->start();
foreach ($rows as $row) {
$this->processRow($row);
$progress->advance();
}
$progress->finish();Formularios Multi-paso con form()
La función form() permite crear formularios completos con navegación entre campos usando Ctrl+U para volver atrás:
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use function Laravel\Prompts\form;
use function Laravel\Prompts\info;
use function Laravel\Prompts\password;
class CreateUserWizardCommand extends Command
{
protected $signature = 'user:wizard';
protected $description = 'Asistente para crear usuarios';
public function handle(): int
{
info('Asistente de Creación de Usuario');
info('Usa Ctrl+U para volver al campo anterior.');
$responses = form()
->text(
label: 'Nombre completo:',
required: true,
validate: fn ($v) => strlen($v) < 2 ? 'Mínimo 2 caracteres.' : null,
name: 'name',
)
->text(
label: 'Email:',
required: true,
validate: function ($value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
return 'Email inválido.';
}
if (User::where('email', $value)->exists()) {
return 'Este email ya está registrado.';
}
return null;
},
name: 'email',
)
->password(
label: 'Contraseña:',
required: true,
validate: fn ($v) => strlen($v) < 8 ? 'Mínimo 8 caracteres.' : null,
name: 'password',
)
->select(
label: 'Rol:',
options: [
'user' => 'Usuario estándar',
'editor' => 'Editor',
'admin' => 'Administrador',
],
default: 'user',
name: 'role',
)
->add(
function (array $responses) {
if ($responses['role'] === 'admin') {
return password(
label: 'PIN de administrador (4 dígitos):',
required: true,
validate: fn ($v) => !preg_match('/^\d{4}$/', $v)
? 'El PIN debe ser de 4 dígitos.'
: null,
);
}
return null;
},
name: 'admin_pin',
)
->confirm(
label: '¿Confirmas la creación?',
default: true,
name: 'confirmed',
)
->submit();
if (!$responses['confirmed']) {
$this->warn('Creación cancelada.');
return self::SUCCESS;
}
$user = User::create([
'name' => $responses['name'],
'email' => $responses['email'],
'password' => Hash::make($responses['password']),
'role' => $responses['role'],
'admin_pin' => $responses['admin_pin'] ?? null,
]);
$this->info("Usuario creado con ID: {$user->id}");
return self::SUCCESS;
}
}El método add() permite añadir campos condicionales basados en respuestas anteriores.
Mensajes Informativos y Tablas
Laravel Prompts incluye funciones para mostrar información:
<?php
use function Laravel\Prompts\alert;
use function Laravel\Prompts\error;
use function Laravel\Prompts\info;
use function Laravel\Prompts\intro;
use function Laravel\Prompts\note;
use function Laravel\Prompts\outro;
use function Laravel\Prompts\table;
use function Laravel\Prompts\warning;
intro('Estado del Sistema');
info('Comprobando servicios...');
table(
headers: ['Servicio', 'Estado'],
rows: [
['Base de datos', 'OK'],
['Cache', 'OK'],
['Queue', '3 workers'],
],
);
warning('Espacio en disco al 75%.');
outro('Comprobación completada.');Transformación de Datos
Puedes transformar los datos antes de la validación con el parámetro transform:
<?php
use function Laravel\Prompts\text;
$slug = text(
label: 'Slug del artículo:',
transform: fn (string $value) => str($value)
->trim()
->lower()
->replace(' ', '-')
->replaceMatches('/[^a-z0-9\-]/', '')
->toString(),
validate: fn (string $value) => strlen($value) < 3
? 'El slug debe tener al menos 3 caracteres.'
: null,
hint: 'Se convertirá automáticamente a formato slug.',
);
$phone = text(
label: 'Teléfono:',
transform: fn (string $value) => preg_replace('/[^0-9]/', '', $value),
validate: fn (string $value) => strlen($value) !== 9
? 'Introduce 9 dígitos.'
: null,
hint: 'Solo se guardarán los números.',
);Testing de Comandos con Prompts
Laravel Prompts incluye Prompt::fake() para testing:
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Prompts\Prompt;
use Tests\TestCase;
class CreateUserWizardTest extends TestCase
{
use RefreshDatabase;
public function test_can_create_user_through_wizard(): void
{
Prompt::fake([
'John Doe', // name
'[email protected]', // email
'SecurePass123', // password
'editor', // role
true, // confirmed
]);
$this->artisan('user:wizard')
->assertSuccessful();
$this->assertDatabaseHas('users', [
'name' => 'John Doe',
'email' => '[email protected]',
'role' => 'editor',
]);
}
public function test_can_cancel_creation(): void
{
Prompt::fake([
'John Doe',
'[email protected]',
'SecurePass123',
'user',
false, // No confirmar
]);
$this->artisan('user:wizard')
->assertSuccessful();
$this->assertDatabaseMissing('users', [
'email' => '[email protected]',
]);
}
public function test_validates_email(): void
{
Prompt::fake([
'John Doe',
'invalid-email', // Inválido
'[email protected]', // Válido
'SecurePass123',
'user',
true,
]);
Prompt::assertOutputContains('Email inválido');
$this->artisan('user:wizard')
->assertSuccessful();
}
public function test_admin_requires_pin(): void
{
Prompt::fake([
'Admin User',
'[email protected]',
'SecurePass123',
'admin',
'1234', // PIN
true,
]);
$this->artisan('user:wizard')
->assertSuccessful();
$this->assertDatabaseHas('users', [
'email' => '[email protected]',
'role' => 'admin',
'admin_pin' => '1234',
]);
}
}Aserciones Disponibles
<?php
// Verificar que se mostró un texto
Prompt::assertOutputContains('Texto esperado');
// Verificar que NO se mostró
Prompt::assertOutputDoesntContain('Texto no esperado');Conclusión
Laravel Prompts transforma la experiencia de crear comandos Artisan. Con validación en tiempo real, formularios navegables y una API intuitiva, puedes crear herramientas CLI profesionales en minutos.
Recuerda que funciona en macOS, Linux y Windows con WSL. En Windows nativo hará fallback automático a los prompts básicos de Symfony.
Los ejemplos de esta guía son 100% funcionales y puedes usarlos como base para tus propios comandos.
Curso Laravel 12
Completo 2026
El único curso 100% actualizado que incluye Laravel 12, Livewire 3, Vue 3, React 19 e Inertia 2. Aprende con proyectos reales y las últimas funcionalidades.
star Incluido en cualquier suscripción