Skip to content

Commit 179967b

Browse files
dxntertaylorotwell
authored andcommitted
[12.x] Add --json option to ScheduleListCommand (laravel#57006)
* feat: add `--json` option to `schedule:list` * formatting --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 986c42a commit 179967b

File tree

2 files changed

+207
-5
lines changed

2 files changed

+207
-5
lines changed

src/Illuminate/Console/Scheduling/ScheduleListCommand.php

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class ScheduleListCommand extends Command
2424
protected $signature = 'schedule:list
2525
{--timezone= : The timezone that times should be displayed in}
2626
{--next : Sort the listed tasks by their next due date}
27+
{--json : Output the scheduled tasks as JSON}
2728
';
2829

2930
/**
@@ -53,21 +54,78 @@ public function handle(Schedule $schedule)
5354
$events = new Collection($schedule->events());
5455

5556
if ($events->isEmpty()) {
56-
$this->components->info('No scheduled tasks have been defined.');
57+
if ($this->option('json')) {
58+
$this->output->writeln('[]');
59+
} else {
60+
$this->components->info('No scheduled tasks have been defined.');
61+
}
5762

5863
return;
5964
}
6065

66+
$timezone = new DateTimeZone($this->option('timezone') ?? config('app.timezone'));
67+
68+
$events = $this->sortEvents($events, $timezone);
69+
70+
$this->option('json')
71+
? $this->displayJson($events, $timezone)
72+
: $this->displayForCli($events, $timezone);
73+
}
74+
75+
/**
76+
* Render the scheduled tasks information as JSON.
77+
*
78+
* @param \Illuminate\Support\Collection $events
79+
* @param \DateTimeZone $timezone
80+
* @return void
81+
*/
82+
protected function displayJson(Collection $events, DateTimeZone $timezone)
83+
{
84+
$this->output->writeln($events->map(function ($event) use ($timezone) {
85+
$nextDueDate = $this->getNextDueDateForEvent($event, $timezone);
86+
87+
$command = $event->command ?? '';
88+
89+
if (! $this->output->isVerbose()) {
90+
$command = $event->normalizeCommand($command);
91+
}
92+
93+
if ($event instanceof CallbackEvent) {
94+
$command = $event->getSummaryForDisplay();
95+
96+
if (in_array($command, ['Closure', 'Callback'])) {
97+
$command = 'Closure at: '.$this->getClosureLocation($event);
98+
}
99+
}
100+
101+
return [
102+
'expression' => $event->expression,
103+
'command' => $command,
104+
'description' => $event->description ?? null,
105+
'next_due_date' => $nextDueDate->format('Y-m-d H:i:s P'),
106+
'next_due_date_human' => $nextDueDate->diffForHumans(),
107+
'timezone' => $timezone->getName(),
108+
'has_mutex' => $event->mutex->exists($event),
109+
'repeat_seconds' => $event->isRepeatable() ? $event->repeatSeconds : null,
110+
];
111+
})->values()->toJson());
112+
}
113+
114+
/**
115+
* Render the scheduled tasks information formatted for the CLI.
116+
*
117+
* @param \Illuminate\Support\Collection $events
118+
* @param \DateTimeZone $timezone
119+
* @return void
120+
*/
121+
protected function displayForCli(Collection $events, DateTimeZone $timezone)
122+
{
61123
$terminalWidth = self::getTerminalWidth();
62124

63125
$expressionSpacing = $this->getCronExpressionSpacing($events);
64126

65127
$repeatExpressionSpacing = $this->getRepeatExpressionSpacing($events);
66128

67-
$timezone = new DateTimeZone($this->option('timezone') ?? config('app.timezone'));
68-
69-
$events = $this->sortEvents($events, $timezone);
70-
71129
$events = $events->map(function ($event) use ($terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone) {
72130
return $this->listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone);
73131
});
@@ -194,6 +252,18 @@ private function sortEvents(\Illuminate\Support\Collection $events, DateTimeZone
194252
: $events;
195253
}
196254

255+
/**
256+
* Render the scheduled tasks information.
257+
*
258+
* @param \Illuminate\Support\Collection $events
259+
* @param \DateTimeZone $timezone
260+
* @return void
261+
*/
262+
protected function display(Collection $events, DateTimeZone $timezone)
263+
{
264+
$this->option('json') ? $this->displayJson($events, $timezone) : $this->displayForCli($events, $timezone);
265+
}
266+
197267
/**
198268
* Get the next due date for an event.
199269
*

tests/Integration/Console/Scheduling/ScheduleListCommandTest.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Illuminate\Tests\Integration\Console\Scheduling;
44

5+
use Illuminate\Console\Application;
56
use Illuminate\Console\Command;
67
use Illuminate\Console\Scheduling\Schedule;
78
use Illuminate\Console\Scheduling\ScheduleListCommand;
@@ -31,6 +32,15 @@ public function testDisplayEmptySchedule()
3132
->expectsOutputToContain('No scheduled tasks have been defined.');
3233
}
3334

35+
public function testDisplayEmptyScheduleAsJson()
36+
{
37+
$this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, ['--json' => true]);
38+
$output = Artisan::output();
39+
40+
$this->assertJson($output);
41+
$this->assertJsonStringEqualsJsonString('[]', $output);
42+
}
43+
3444
public function testDisplaySchedule()
3545
{
3646
$this->schedule->command(FooCommand::class)->quarterly();
@@ -65,6 +75,128 @@ public function testDisplaySchedule()
6575
->expectsOutput(' * * * * * Closure at: '.$closureFilePath.':'.$closureLineNumber.' Next Due: 1 minute from now');
6676
}
6777

78+
public function testDisplayScheduleAsJson()
79+
{
80+
$this->schedule->command(FooCommand::class)->quarterly();
81+
$this->schedule->command('inspire')->twiceDaily(14, 18);
82+
$this->schedule->command('foobar', ['a' => 'b'])->everyMinute();
83+
$this->schedule->job(FooJob::class)->everyMinute();
84+
$this->schedule->job(new FooParamJob('test'))->everyMinute();
85+
$this->schedule->job(FooJob::class)->name('foo-named-job')->everyMinute();
86+
$this->schedule->job(new FooParamJob('test'))->name('foo-named-param-job')->everyMinute();
87+
$this->schedule->command('inspire')->cron('0 9,17 * * *');
88+
$this->schedule->call(fn () => '')->everyMinute();
89+
90+
$this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, ['--json' => true]);
91+
$output = Artisan::output();
92+
93+
$this->assertJson($output);
94+
$data = json_decode($output, true);
95+
96+
$this->assertIsArray($data);
97+
$this->assertCount(9, $data);
98+
99+
$this->assertSame('0 0 1 1-12/3 *', $data[0]['expression']);
100+
$this->assertNull($data[0]['repeat_seconds']);
101+
$this->assertSame('php artisan foo:command', $data[0]['command']);
102+
$this->assertSame('This is the description of the command.', $data[0]['description']);
103+
$this->assertStringContainsString('2023-04-01 00:00:00', $data[0]['next_due_date']);
104+
$this->assertSame('3 months from now', $data[0]['next_due_date_human']);
105+
$this->assertFalse($data[0]['has_mutex']);
106+
107+
$this->assertSame('* * * * *', $data[2]['expression']);
108+
$this->assertSame('php artisan foobar a='.ProcessUtils::escapeArgument('b'), $data[2]['command']);
109+
$this->assertNull($data[2]['description']);
110+
$this->assertSame('1 minute from now', $data[2]['next_due_date_human']);
111+
112+
$this->assertSame('Illuminate\Tests\Integration\Console\Scheduling\FooJob', $data[3]['command']);
113+
114+
$this->assertSame('foo-named-job', $data[5]['command']);
115+
116+
$this->assertStringContainsString('Closure at:', $data[8]['command']);
117+
$this->assertStringContainsString('ScheduleListCommandTest.php', $data[8]['command']);
118+
}
119+
120+
public function testDisplayScheduleWithSortAsJson()
121+
{
122+
$this->schedule->command(FooCommand::class)->quarterly();
123+
$this->schedule->command('inspire')->twiceDaily(14, 18);
124+
$this->schedule->command('foobar', ['a' => 'b'])->everyMinute();
125+
126+
$this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, [
127+
'--next' => true,
128+
'--json' => true,
129+
]);
130+
$output = Artisan::output();
131+
132+
$this->assertJson($output);
133+
$data = json_decode($output, true);
134+
135+
$this->assertIsArray($data);
136+
$this->assertCount(3, $data);
137+
138+
$this->assertSame('* * * * *', $data[0]['expression']);
139+
$this->assertSame('1 minute from now', $data[0]['next_due_date_human']);
140+
$this->assertSame('php artisan foobar a='.ProcessUtils::escapeArgument('b'), $data[0]['command']);
141+
142+
$this->assertSame('0 14,18 * * *', $data[1]['expression']);
143+
$this->assertSame('14 hours from now', $data[1]['next_due_date_human']);
144+
$this->assertSame('php artisan inspire', $data[1]['command']);
145+
146+
$this->assertSame('0 0 1 1-12/3 *', $data[2]['expression']);
147+
$this->assertSame('3 months from now', $data[2]['next_due_date_human']);
148+
$this->assertSame('php artisan foo:command', $data[2]['command']);
149+
}
150+
151+
public function testDisplayScheduleAsJsonWithTimezone()
152+
{
153+
$this->schedule->command('inspire')->daily();
154+
155+
$this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, [
156+
'--timezone' => 'America/Chicago',
157+
'--json' => true,
158+
]);
159+
$output = Artisan::output();
160+
161+
$this->assertJson($output);
162+
$data = json_decode($output, true);
163+
164+
$this->assertIsArray($data);
165+
$this->assertCount(1, $data);
166+
$this->assertSame('America/Chicago', $data[0]['timezone']);
167+
$this->assertStringContainsString('-06:00', $data[0]['next_due_date']);
168+
$this->assertSame('php artisan inspire', $data[0]['command']);
169+
}
170+
171+
public function testDisplayScheduleAsJsonInVerboseMode()
172+
{
173+
$this->schedule->command(FooCommand::class)->quarterly();
174+
$this->schedule->command('inspire')->everyMinute();
175+
$this->schedule->call(fn () => '')->everyMinute();
176+
177+
$this->withoutMockingConsoleOutput()->artisan(ScheduleListCommand::class, [
178+
'--json' => true,
179+
'-v' => true,
180+
]);
181+
$output = Artisan::output();
182+
183+
$this->assertJson($output);
184+
$data = json_decode($output, true);
185+
186+
$this->assertIsArray($data);
187+
$this->assertCount(3, $data);
188+
189+
$this->assertSame('0 0 1 1-12/3 *', $data[0]['expression']);
190+
$this->assertSame(Application::phpBinary().' '.Application::artisanBinary().' foo:command', $data[0]['command']);
191+
$this->assertSame('This is the description of the command.', $data[0]['description']);
192+
193+
$this->assertSame('* * * * *', $data[1]['expression']);
194+
$this->assertSame(Application::phpBinary().' '.Application::artisanBinary().' inspire', $data[1]['command']);
195+
196+
$this->assertStringContainsString('Closure at:', $data[2]['command']);
197+
$this->assertStringContainsString('ScheduleListCommandTest.php', $data[2]['command']);
198+
}
199+
68200
public function testDisplayScheduleWithSort()
69201
{
70202
$this->schedule->command(FooCommand::class)->quarterly();

0 commit comments

Comments
 (0)