Chapter 17 will build a command that rewrites rows, a backfill. Three habits apply to it and to any command that changes data in production.
A dry run. --dry-run prints what would change and changes nothing. Run it first, read the output, then run it for real.
Safe to run again. A command interrupted by a deploy or a lost connection will be run a second time. It has to carry on, not start over or do the work twice.
Confirmation where it is destructive. A command that deletes should ask before it does, and refuse to run unattended in production unless told to:
if (! $this->confirmToProceed()) {
return self::FAILURE;
}
Add the ConfirmableTrait to the command and that line prompts in production and passes without asking elsewhere. Declare {--force} in the signature and a script that means it can skip the prompt. It is how migrate itself behaves.
Testing Commands and Schedules
A command is tested like an endpoint: call it, then assert on the output, the exit code, and what is now true.
// tests/Feature/IssueConsumerTokenTest.php
it('issues a token from the console', function () {
$consumer = Consumer::factory()->create();
$this->artisan('consumers:token', [
'consumer' => $consumer->id,
'--ability' => ['licenses:read'],
])
->expectsOutputToContain('Shown once')
->assertSuccessful();
expect($consumer->tokens()->count())->toBe(1);
});
it('fails for a consumer that does not exist', function () {
$this->artisan('consumers:token', ['consumer' => 999])
->assertFailed();
});
For the schedule, there is a command that runs one task now, without waiting for its time:
php artisan schedule:test
It lists the scheduled tasks and runs the one you pick. Use it after changing a schedule, before you trust the clock to do it for you.
And for anything that depends on time passing, Laravel can move the clock:
it('prunes requests past the retention window', function () {
ApiRequest::factory()->create();
$this->travel(91)->days();
$this->artisan('model:prune')->assertSuccessful();
expect(ApiRequest::count())->toBe(0);
});
That test proves the retention policy Chapter 17 puts on the model, which keeps a request for ninety days. ApiRequest gets its factory the usual way, from make:model -f. It fails the day someone changes the window without meaning to.
Chapter 11 Summary
- A command is a controller for the terminal: a declared signature, injected dependencies, almost no logic.
- When an operation has two ways in, put its rules in one method and call it from both.
- Return a real exit code. It is the only part of the output that machines read.
- One cron entry runs the scheduler. The schedule is code in
routes/console.php, andschedule:listshows what it really is. - Use
withoutOverlapping()andonOneServer(), with a cache shared between servers. - The scheduler dispatches. Heavy work goes to the queue.
- A task that never runs raises no error. Put a heartbeat on every task whose absence would hurt.
- Commands that change data get a dry run, are safe to run again, and confirm before destroying.