Saltar al contenido

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/prompts

No 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.

school Curso completo

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.

access_time 8 horas de contenido
layers 4 tecnologías en 1
update 100% actualizado
code Proyectos prácticos
Ver Curso Laravel 12 arrow_forward

star Incluido en cualquier suscripción

Rutas de aprendizaje