From d13c34865f85c473781f418e0be4fe592400f68e Mon Sep 17 00:00:00 2001 From: Francisco Madeira Date: Sat, 12 Mar 2022 01:22:40 +0000 Subject: [PATCH] [9.x] Redesign `php artisan schedule:list` Command. (#41445) * Redesign `php artisan schedule:list` Command. * style: fix * style: fix * fix: Remove `SHELL_VERBOSITY` from env after tests. * formatting * empty satte * adjust wording * Clear `AliasLoader` after the tests. * move test * remove alias clear * delete line * fix: Replace " also (Windows) * style: fixes * Improve RegExp for windows machines. * style: fixes Co-authored-by: Taylor Otwell --- .../Scheduling/ScheduleListCommand.php | 139 +++++++++++++++--- .../Scheduling/ScheduleListCommandTest.php | 64 ++++++++ 2 files changed, 185 insertions(+), 18 deletions(-) create mode 100644 tests/Integration/Console/Scheduling/ScheduleListCommandTest.php diff --git a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php index feb7f428dfbc..73ef1f0ecde8 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -4,8 +4,10 @@ use Cron\CronExpression; use DateTimeZone; +use Illuminate\Console\Application; use Illuminate\Console\Command; use Illuminate\Support\Carbon; +use Symfony\Component\Console\Terminal; class ScheduleListCommand extends Command { @@ -23,6 +25,13 @@ class ScheduleListCommand extends Command */ protected $description = 'List the scheduled commands'; + /** + * The terminal width resolver callback. + * + * @var \Closure|null + */ + protected static $terminalWidthResolver; + /** * Execute the console command. * @@ -33,25 +42,119 @@ class ScheduleListCommand extends Command */ public function handle(Schedule $schedule) { - foreach ($schedule->events() as $event) { - $rows[] = [ - $event->command, - $event->expression, - $event->description, - (new CronExpression($event->expression)) - ->getNextRunDate(Carbon::now()->setTimezone($event->timezone)) - ->setTimezone(new DateTimeZone($this->option('timezone') ?? config('app.timezone'))) - ->format('Y-m-d H:i:s P'), - $event->mutex->exists($event) ? 'Yes' : '', - ]; + $events = collect($schedule->events()); + $terminalWidth = $this->getTerminalWidth(); + $expressionSpacing = $this->getCronExpressionSpacing($events); + + $events = $events->map(function ($event) use ($terminalWidth, $expressionSpacing) { + $expression = $this->formatCronExpression($event->expression, $expressionSpacing); + + $command = $event->command; + + if (! $this->output->isVerbose()) { + $command = str_replace( + Application::artisanBinary(), + preg_replace("#['\"]#", '', Application::artisanBinary()), + str_replace(Application::phpBinary(), 'php', $event->command) + ); + } + + $command = mb_strlen($command) > 1 ? "{$command} " : ''; + + $nextDueDateLabel = 'Next Due:'; + + $nextDueDate = Carbon::create((new CronExpression($event->expression)) + ->getNextRunDate(Carbon::now()->setTimezone($event->timezone)) + ->setTimezone(new DateTimeZone($this->option('timezone') ?? config('app.timezone'))) + ); + + $nextDueDate = $this->output->isVerbose() + ? $nextDueDate->format('Y-m-d H:i:s P') + : $nextDueDate->diffForHumans(); + + $hasMutex = $event->mutex->exists($event) ? 'Has Mutex › ' : ''; + + $dots = str_repeat('.', max( + $terminalWidth - mb_strlen($expression.$command.$nextDueDateLabel.$nextDueDate.$hasMutex) - 8, 0 + )); + + // Highlight the parameters... + $command = preg_replace("#(=['\"]?)([^'\"]+)(['\"]?)#", '$1$2$3', $command); + + return [sprintf( + ' %s %s%s %s%s %s', + $expression, + $command, + $dots, + $hasMutex, + $nextDueDateLabel, + $nextDueDate + ), $this->output->isVerbose() && mb_strlen($event->description) > 1 ? sprintf( + ' %s%s %s', + str_repeat(' ', mb_strlen($expression) + 2), + '⇁', + $event->description + ) : '']; + }); + + if ($events->isEmpty()) { + return $this->comment('No scheduled tasks have been defined.'); } - $this->table([ - 'Command', - 'Interval', - 'Description', - 'Next Due', - 'Has Mutex', - ], $rows ?? []); + $this->output->writeln( + $events->flatten()->filter()->prepend('')->push('')->toArray() + ); + } + + /** + * Gets the spacing to be used on each event row. + * + * @param \Illuminate\Support\Collection $events + * @return array + */ + private function getCronExpressionSpacing($events) + { + $rows = $events->map(fn ($event) => array_map('mb_strlen', explode(' ', $event->expression))); + + return collect($rows[0] ?? [])->keys()->map(fn ($key) => $rows->max($key)); + } + + /** + * Formats the cron expression based on the spacing provided. + * + * @param string $expression + * @param array $spacing + * @return string + */ + private function formatCronExpression($expression, $spacing) + { + $expression = explode(' ', $expression); + + return collect($spacing) + ->map(fn ($length, $index) => $expression[$index] = str_pad($expression[$index], $length)) + ->implode(' '); + } + + /** + * Get the terminal width. + * + * @return int + */ + public static function getTerminalWidth() + { + return is_null(static::$terminalWidthResolver) + ? (new Terminal)->getWidth() + : call_user_func(static::$terminalWidthResolver); + } + + /** + * Set a callback that should be used when resolving the terminal width. + * + * @param \Closure|null $resolver + * @return void + */ + public static function resolveTerminalWidthUsing($resolver) + { + static::$terminalWidthResolver = $resolver; } } diff --git a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php new file mode 100644 index 000000000000..4a4d7efed7c8 --- /dev/null +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -0,0 +1,64 @@ +startOfYear()); + ScheduleListCommand::resolveTerminalWidthUsing(fn () => 80); + + $this->schedule = $this->app->make(Schedule::class); + } + + public function testDisplaySchedule() + { + $this->schedule->call(fn () => '')->everyMinute(); + $this->schedule->command(FooCommand::class)->quarterly(); + $this->schedule->command('inspire')->twiceDaily(14, 18); + $this->schedule->command('foobar', ['a' => 'b'])->everyMinute(); + + $this->artisan(ScheduleListCommand::class) + ->assertSuccessful() + ->expectsOutput(' * * * * * ............................ Next Due: 1 minute from now') + ->expectsOutput(' 0 0 1 1-12/3 * php artisan foo:command .... Next Due: 3 months from now') + ->expectsOutput(' 0 14,18 * * * php artisan inspire ........ Next Due: 14 hours from now') + ->expectsOutput(' * * * * * php artisan foobar a='.ProcessUtils::escapeArgument('b').' ... Next Due: 1 minute from now'); + } + + public function testDisplayScheduleInVerboseMode() + { + $this->schedule->command(FooCommand::class)->everyMinute(); + + $this->artisan(ScheduleListCommand::class, ['-v' => true]) + ->assertSuccessful() + ->expectsOutputToContain('Next Due: '.now()->setMinutes(1)->format('Y-m-d H:i:s P')) + ->expectsOutput(' ⇁ This is the description of the command.'); + } + + protected function tearDown(): void + { + parent::tearDown(); + + putenv('SHELL_VERBOSITY'); + + ScheduleListCommand::resolveTerminalWidthUsing(null); + } +} + +class FooCommand extends Command +{ + protected $signature = 'foo:command'; + + protected $description = 'This is the description of the command.'; +}