Una API tiene un lado que sus consumidores nunca ven. Alguien emite un token desde una terminal. Algo purga filas viejas cada noche. En el servicio que aprovisiona sitios, un barrido busca trabajo que se quedó atascado. Nada de eso llega como una petición HTTP, y todo ello forma parte del sistema que operas.
Laravel le da a ese lado dos cosas: los comandos de Artisan, para el trabajo que empieza una persona, y el planificador (scheduler), para el trabajo que empieza el reloj. Este capítulo trata a ambos como el resto del libro trató a los endpoints: delgados, probados e incapaces de fallar sin que se note. Esa última parte cuesta trabajo, porque una tarea programada que deja de ejecutarse no lanza ninguna excepción.
Un comando es un controlador para la terminal
El Capítulo 5 les dio a los operadores un endpoint para emitir tokens. Necesitan lo mismo en una terminal, aunque solo sea para el primer token de operador, y debería ser un comando que cualquiera pueda repetir, no algo tecleado de memoria en una consola.
php artisan make:command IssueConsumerToken
// app/Console/Commands/IssueConsumerToken.php
#[Signature('consumers:token
{consumer : The consumer ID}
{--ability=* : Abilities to grant}
{--days=365 : Days until the token expires}')]
#[Description('Issue an API token for a consumer')]
class IssueConsumerToken extends Command
{
// ...
}
En Laravel 13 la firma y la descripción son atributos sobre la clase. La firma es el contrato del comando, igual que un FormRequest lo es de un endpoint: nombra cada argumento y cada opción, describe cada uno, y Laravel construye --help a partir de ella. (Documentación de Laravel: Artisan Console.)
public function handle(): int
{
$consumer = Consumer::find($this->argument('consumer'));
if ($consumer === null) {
$this->error('No such consumer.');
return self::FAILURE;
}
$token = $consumer->issueToken(
name: 'issued from the console',
abilities: $this->option('ability'),
days: (int) $this->option('days'),
);
$this->info('Shown once. Store it now:');
$this->line($token->plainTextToken);
return self::SUCCESS;
}
Como un método de controlador, traduce su entrada, le pasa el trabajo a otra cosa e informa del resultado.
Un método, dos puertas
Ahora hay dos maneras de emitir un token: el endpoint del Capítulo 5 y este comando. Si cada una contuviera la lógica, se irían separando. Una añadiría una caducidad por defecto y la otra no.
Así que la lógica no vive en ninguna. Las dos llaman a Consumer::issueToken(), el método que el Capítulo 5 puso en el modelo, y ese método es donde corresponde una regla cuando las dos puertas deben obedecerla. El endpoint validaba las habilidades en su FormRequest. El comando no tiene FormRequest, así que la comprobación se muda a donde ninguno de los dos pueda saltársela:
// app/Models/Consumer.php
public function issueToken(
string $name,
array $abilities,
int $days = 365,
): NewAccessToken {
if ($abilities === []) {
throw new InvalidArgumentException('No abilities.');
}
foreach ($abilities as $ability) {
Ability::from($ability); // lanza si no existe
}
return $this->createToken(
name: $name,
abilities: $abilities,
expiresAt: now()->addDays($days),
);
}
Una lista vacía se rechaza porque Sanctum la leería como «ninguna habilidad», y un operador que olvidara la opción recibiría un token que no puede hacer nada, sin ningún aviso. Una habilidad desconocida se rechaza porque una errata haría lo mismo con un permiso.
El FormRequest sigue validando. Su trabajo es darle a quien llama por HTTP un 422 claro. La comprobación del modelo es la que no se puede olvidar. Cuando una operación tiene dos entradas, la regla va detrás de las dos.
Los códigos de salida son los códigos de estado del comando
self::SUCCESS es cero. self::FAILURE es uno. Ese valor de retorno es lo único que un script de despliegue, un planificador o una herramienta de monitorización saben de lo que pasó.
Un comando que imprime «Something went wrong» en rojo y devuelve éxito les ha dicho a todas las máquinas que lo observan que todo va bien. Trata el código de salida como el Capítulo 2 trató el estado HTTP: es la parte de la respuesta que lee el software. El comando de arriba devuelve FAILURE para un consumidor que no existe, y lo hace con un mensaje, no dejando escapar una excepción. El barrido de trabajo atascado del Capítulo 10 devuelve FAILURE cuando se niega a actuar, y esa salida distinta de cero es lo que permite que el planificador lo note.