> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Consola Artisan

> Explicamos cómo crear comandos CLI personalizados usando la consola Artisan de Laravel.

## ¿Qué es Artisan?

Artisan es la herramienta de línea de comandos (CLI) incluida en Laravel.
Existe como script `artisan` en la raíz del proyecto y ofrece numerosos comandos para agilizar el desarrollo y la operación.

Para ver la lista de comandos disponibles, usa `list`.

```shell theme={null}
php artisan list
```

Para consultar el uso de un comando, antepón `help`.

```shell theme={null}
php artisan help migrate
```

<Info>
  Si utilizas Laravel Sail, usa `sail artisan` en lugar de `php artisan`.
  Los comandos se ejecutan dentro del contenedor Docker.

  ```shell theme={null}
  ./vendor/bin/sail artisan list
  ```
</Info>

## Tinker

[Laravel Tinker](https://github.com/laravel/tinker) es un REPL que te permite interactuar en la línea de comandos con toda la aplicación: modelos Eloquent, jobs, eventos, etc.

```shell theme={null}
php artisan tinker
```

Una vez dentro puedes ejecutar código PHP directamente.

```php theme={null}
// Obtener y comprobar un usuario
>>> App\Models\User::find(1)
// Crear un registro con una factory
>>> App\Models\User::factory()->create()
// Invocar directamente una clase de servicio
>>> app(App\Services\OrderService::class)->process(1)
```

<Tip>
  Tinker modifica realmente los datos. Ten cuidado al ejecutarlo en producción.
  Es apropiado para pruebas y depuración en local o staging.
</Tip>

## Crear comandos personalizados

### Generar el comando

Con `make:command` generas el esqueleto de una clase de comando.

```shell theme={null}
php artisan make:command ImportProducts
```

Se crea `app/Console/Commands/ImportProducts.php`.

### Estructura básica

La clase generada tiene tres elementos principales: `$signature`, `$description` y `handle()`.

```php theme={null}
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class ImportProducts extends Command
{
    /**
     * Nombre del comando y definición de argumentos/opciones
     */
    protected $signature = 'import:products';

    /**
     * Descripción del comando (se muestra en php artisan list)
     */
    protected $description = 'Importa datos de productos desde un CSV';

    /**
     * Lógica de ejecución del comando
     */
    public function handle(): void
    {
        $this->info('Comenzando la importación...');
        // Escribe aquí el procesamiento
        $this->info('Terminado.');
    }
}
```

<Info>
  En Laravel 13 también puedes usar atributos PHP.

  ```php theme={null}
  use Illuminate\Console\Attributes\Description;
  use Illuminate\Console\Attributes\Signature;

  #[Signature('import:products')]
  #[Description('Importa datos de productos desde un CSV')]
  class ImportProducts extends Command
  {
      public function handle(): void
      {
          // ...
      }
  }
  ```
</Info>

### Definición de argumentos y opciones

Define el nombre, argumentos y opciones del comando en `$signature`.

```php theme={null}
protected $signature = 'import:products
                        {file : Ruta del CSV a importar}
                        {--limit= : Número máximo de filas a importar}
                        {--dry-run : No guarda realmente en la BD (solo para comprobar)}';
```

| Notación              | Tipo                            | Descripción                             |
| --------------------- | ------------------------------- | --------------------------------------- |
| `{file}`              | Argumento obligatorio           | Si se omite, es un error                |
| `{file?}`             | Argumento opcional              | Si se omite, `null`                     |
| `{file=products.csv}` | Argumento con valor por defecto | Se usa el valor por defecto si se omite |
| `{--queue}`           | Opción tipo flag                | `true` si se indica, `false` si no      |
| `{--limit=}`          | Opción con valor                | Pasa el valor así: `--limit=100`        |
| `{--limit=50}`        | Opción con valor por defecto    | Si se omite, `50`                       |
| `{--Q\|queue}`        | Opción con atajo                | También se puede indicar con `-Q`       |

### Obtener argumentos y opciones

Usa `argument()` y `option()` dentro de `handle()`.

```php theme={null}
public function handle(): void
{
    $file    = $this->argument('file');       // Obtener argumento
    $limit   = $this->option('limit');        // Obtener opción
    $dryRun  = $this->option('dry-run');      // Opción flag (bool)

    $this->info("Archivo: {$file}");

    if ($dryRun) {
        $this->warn('Modo dry-run: se omitirá el guardado en la BD');
    }
}
```

### `input()` — accesores tipados

Con `input()` puedes obtener los argumentos y opciones del comando mediante accesores tipados, igual que en una HTTP request.

```php theme={null}
use App\Enums\ReportType;

public function handle(): void
{
    // Obtiene argumentos/opciones como instancia CommandInput
    $from = $this->input()->date('from');
    $type = $this->input()->enum('type', ReportType::class);
    $limit = $this->input()->integer('limit');
}
```

Si solo quieres uno con nombre, pásaselo directamente (busca en argumentos y opciones).

```php theme={null}
$queue = $this->input('queue', 'default');
```

<Tip>
  `argument()` y `option()` devuelven strings; los accesores tipados de `input()` convierten al tipo adecuado. Muy útil para manejar de forma segura fechas, números o Enums.
</Tip>

## Interacción con el usuario

### Mensajes por pantalla

Existen métodos para imprimir mensajes con colores.

```php theme={null}
$this->info('El proceso terminó correctamente');   // Verde
$this->warn('Esta operación no se puede deshacer'); // Amarillo
$this->error('Se ha producido un error');           // Rojo
$this->line('Texto normal');                        // Sin color
$this->comment('Información adicional');            // Gris claro
```

### Preguntar al usuario

Puedes recibir entradas del usuario de forma interactiva.

```php theme={null}
// Entrada de texto
$name = $this->ask('Introduce el nombre del responsable');

// Con valor por defecto
$env = $this->ask('Selecciona el entorno', 'production');

// Información sensible (no se muestra al escribir)
$password = $this->secret('Introduce la clave de API');

// Confirmación Sí/No
if (! $this->confirm('¿Importar a la BD de producción?')) {
    $this->info('Cancelado.');
    return;
}

// Elegir de entre opciones
$format = $this->choice('Elige el formato de salida', ['csv', 'json', 'xml'], 0);
```

### Barra de progreso

Muestra fácilmente el progreso al procesar grandes cantidades de datos.

```php theme={null}
use App\Models\Product;

// Al pasar una colección, se muestra la barra automáticamente
$this->withProgressBar(Product::cursor(), function (Product $product) {
    $this->processProduct($product);
});
```

Para un control manual más fino:

```php theme={null}
$total = Product::count();
$bar = $this->output->createProgressBar($total);
$bar->start();

Product::cursor()->each(function (Product $product) use ($bar) {
    $this->processProduct($product);
    $bar->advance();
});

$bar->finish();
$this->newLine();
```

## Casos prácticos

### Comando de importación de datos

Ejemplo de comando que importa productos desde un CSV.

<Steps>
  <Step title="Generar el comando">
    ```shell theme={null}
    php artisan make:command ImportProducts
    ```
  </Step>

  <Step title="Implementar el comando">
    ```php theme={null}
    <?php

    namespace App\Console\Commands;

    use App\Models\Product;
    use Illuminate\Console\Command;

    class ImportProducts extends Command
    {
        protected $signature = 'import:products
                                {file : Ruta del CSV}
                                {--limit= : Número máximo de filas a importar}
                                {--dry-run : Solo comprobar (no guarda en la BD)}';

        protected $description = 'Importa datos de productos desde un CSV';

        public function handle(): void
        {
            $filePath = $this->argument('file');
            $limit    = $this->option('limit') ? (int) $this->option('limit') : null;
            $dryRun   = $this->option('dry-run');

            if (! file_exists($filePath)) {
                $this->error("No se encuentra el archivo: {$filePath}");
                return;
            }

            // Lee el CSV con funciones nativas de PHP
            $handle = fopen($filePath, 'r');
            $headers = fgetcsv($handle); // Primera línea como cabeceras
            $rows = [];
            while (($row = fgetcsv($handle)) !== false) {
                $rows[] = array_combine($headers, $row);
                if ($limit !== null && count($rows) >= $limit) {
                    break;
                }
            }
            fclose($handle);

            $count = 0;

            $this->withProgressBar($rows, function (array $row) use ($dryRun, &$count) {
                if (! $dryRun) {
                    Product::updateOrCreate(
                        ['sku' => $row['sku']],
                        [
                            'name'  => $row['name'],
                            'price' => $row['price'],
                        ]
                    );
                }
                $count++;
            });

            $this->newLine();

            if ($dryRun) {
                $this->warn("{$count} filas encontradas (dry-run: no se ha guardado nada)");
            } else {
                $this->info("Importación completada: {$count} filas.");
            }
        }
    }
    ```
  </Step>

  <Step title="Ejecutar el comando">
    ```shell theme={null}
    # Importación normal
    php artisan import:products storage/products.csv

    # Limitar el número de filas
    php artisan import:products storage/products.csv --limit=100

    # Comprobar en dry-run
    php artisan import:products storage/products.csv --dry-run
    ```
  </Step>
</Steps>

### Comando de mantenimiento periódico

Ejemplo para eliminar datos antiguos periódicamente.

```php theme={null}
<?php

namespace App\Console\Commands;

use App\Models\Order;
use Illuminate\Console\Command;

class PruneOldOrders extends Command
{
    protected $signature = 'orders:prune {--days=90 : Antigüedad mínima de los datos a eliminar}';

    protected $description = 'Elimina los pedidos completados con más antigüedad que la indicada';

    public function handle(): void
    {
        $days = (int) $this->option('days');

        if (! $this->confirm("¿Eliminar pedidos completados con más de {$days} días?")) {
            $this->info('Cancelado.');
            return;
        }

        $deleted = Order::where('status', 'completed')
            ->where('created_at', '<', now()->subDays($days))
            ->delete();

        $this->info("{$deleted} pedidos eliminados.");
    }
}
```

### Combinación con la programación de tareas

Puedes ejecutar tus comandos con la programación de tareas en `routes/console.php`.
Consulta [Programación de tareas](/es/scheduling) para más detalles.

```php theme={null}
use Illuminate\Support\Facades\Schedule;

// Diario a las 2 de la madrugada
Schedule::command('orders:prune --days=90')->dailyAt('2:00');

// Cada lunes a las 9
Schedule::command('import:products storage/weekly.csv')->weeklyOn(1, '9:00');
```

## Pruebas de comandos

Usa las utilidades de prueba de Laravel para verificar el funcionamiento de los comandos Artisan.

```php theme={null}
<?php

namespace Tests\Feature\Commands;

use App\Models\Order;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PruneOldOrdersTest extends TestCase
{
    use RefreshDatabase;

    public function test_it_deletes_old_completed_orders(): void
    {
        // Crea un pedido completado hace más de 90 días
        Order::factory()->create([
            'status'     => 'completed',
            'created_at' => now()->subDays(100),
        ]);

        // Pedido reciente (no debería eliminarse)
        Order::factory()->create([
            'status'     => 'completed',
            'created_at' => now()->subDays(10),
        ]);

        $this->artisan('orders:prune', ['--days' => 90])
            ->expectsConfirmation('¿Eliminar pedidos completados con más de 90 días?', 'yes')
            ->expectsOutput('1 pedidos eliminados.')
            ->assertExitCode(0);

        $this->assertDatabaseCount('orders', 1);
    }

    public function test_it_cancels_when_user_declines(): void
    {
        Order::factory()->create(['status' => 'completed', 'created_at' => now()->subDays(100)]);

        $this->artisan('orders:prune', ['--days' => 90])
            ->expectsConfirmation('¿Eliminar pedidos completados con más de 90 días?', 'no')
            ->expectsOutput('Cancelado.')
            ->assertExitCode(0);

        $this->assertDatabaseCount('orders', 1);
    }
}
```

<Info>
  Ejemplos de aserciones disponibles con `artisan()`:

  | Método                              | Descripción                                       |
  | ----------------------------------- | ------------------------------------------------- |
  | `expectsOutput('...')`              | Comprueba que se imprime el texto indicado        |
  | `expectsQuestion('?', 'respuesta')` | Simula la entrada de `ask()`                      |
  | `expectsConfirmation('?', 'yes')`   | Simula la respuesta de `confirm()`                |
  | `expectsChoice('?', 'opción')`      | Simula la elección de `choice()`                  |
  | `assertExitCode(0)`                 | Verifica el código de salida (0 = éxito)          |
  | `assertFailed()`                    | Verifica que el código de salida es distinto de 0 |
</Info>

## Comandos habituales

```shell theme={null}
# Crear un comando nuevo
php artisan make:command CommandName

# Lista de comandos disponibles
php artisan list

# Ayuda de un comando
php artisan help command:name

# Iniciar Tinker
php artisan tinker
```


## Related topics

- [Laravel Prompts](/es/prompts.md)
- [Procesos](/es/processes.md)
- [Pruebas de consola](/es/console-tests.md)
- [Ciclo de vida de la solicitud](/es/lifecycle.md)
- [Pruebas de navegador (Dusk)](/es/dusk.md)
