diff --git a/composer.json b/composer.json index 74f27ac25ba7..0082c4481806 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "league/flysystem": "^3.0.16", "monolog/monolog": "^2.0", "nesbot/carbon": "^2.53.1", + "nunomaduro/termwind": "^1.13", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", "psr/simple-cache": "^1.0|^2.0|^3.0", diff --git a/src/Illuminate/Auth/Console/ClearResetsCommand.php b/src/Illuminate/Auth/Console/ClearResetsCommand.php index 9c3d9e033987..2ea96681f8e7 100644 --- a/src/Illuminate/Auth/Console/ClearResetsCommand.php +++ b/src/Illuminate/Auth/Console/ClearResetsCommand.php @@ -42,6 +42,6 @@ public function handle() { $this->laravel['auth.password']->broker($this->argument('name'))->getRepository()->deleteExpired(); - $this->info('Expired reset tokens cleared successfully.'); + $this->components->info('Expired reset tokens cleared successfully.'); } } diff --git a/src/Illuminate/Cache/Console/CacheTableCommand.php b/src/Illuminate/Cache/Console/CacheTableCommand.php index ea18d4f20812..19b591bdda36 100644 --- a/src/Illuminate/Cache/Console/CacheTableCommand.php +++ b/src/Illuminate/Cache/Console/CacheTableCommand.php @@ -73,7 +73,7 @@ public function handle() $this->files->put($fullPath, $this->files->get(__DIR__.'/stubs/cache.stub')); - $this->info('Migration created successfully.'); + $this->components->info('Migration created successfully.'); $this->composer->dumpAutoloads(); } diff --git a/src/Illuminate/Cache/Console/ClearCommand.php b/src/Illuminate/Cache/Console/ClearCommand.php index ca8d2a27bdb6..7d3336c712a9 100755 --- a/src/Illuminate/Cache/Console/ClearCommand.php +++ b/src/Illuminate/Cache/Console/ClearCommand.php @@ -82,14 +82,14 @@ public function handle() $this->flushFacades(); if (! $successful) { - return $this->error('Failed to clear cache. Make sure you have the appropriate permissions.'); + return $this->components->error('Failed to clear cache. Make sure you have the appropriate permissions.'); } $this->laravel['events']->dispatch( 'cache:cleared', [$this->argument('store'), $this->tags()] ); - $this->info('Application cache cleared successfully.'); + $this->components->info('Application cache cleared successfully.'); } /** diff --git a/src/Illuminate/Cache/Console/ForgetCommand.php b/src/Illuminate/Cache/Console/ForgetCommand.php index 41e7adbdee14..c7fc830cd999 100755 --- a/src/Illuminate/Cache/Console/ForgetCommand.php +++ b/src/Illuminate/Cache/Console/ForgetCommand.php @@ -65,6 +65,6 @@ public function handle() $this->argument('key') ); - $this->info('The ['.$this->argument('key').'] key has been removed from the cache.'); + $this->components->info('The ['.$this->argument('key').'] key has been removed from the cache.'); } } diff --git a/src/Illuminate/Console/Command.php b/src/Illuminate/Console/Command.php index 6d9ae8c89381..0b5242658892 100755 --- a/src/Illuminate/Console/Command.php +++ b/src/Illuminate/Console/Command.php @@ -2,6 +2,7 @@ namespace Illuminate\Console; +use Illuminate\Console\View\Components\Factory; use Illuminate\Support\Traits\Macroable; use Symfony\Component\Console\Command\Command as SymfonyCommand; use Symfony\Component\Console\Input\InputInterface; @@ -117,6 +118,8 @@ public function run(InputInterface $input, OutputInterface $output): int OutputStyle::class, ['input' => $input, 'output' => $output] ); + $this->components = new Factory($this->output); + return parent::run( $this->input = $input, $this->output ); diff --git a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php index a360c281a98a..4182a007ed3a 100644 --- a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php +++ b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php @@ -28,17 +28,17 @@ protected function addTestOptions() * Create the matching test case if requested. * * @param string $path - * @return void + * @return bool */ protected function handleTestCreation($path) { if (! $this->option('test') && ! $this->option('pest')) { - return; + return false; } - $this->call('make:test', [ + return $this->callSilent('make:test', [ 'name' => Str::of($path)->after($this->laravel['path'])->beforeLast('.php')->append('Test')->replace('\\', '/'), '--pest' => $this->option('pest'), - ]); + ]) == 0; } } diff --git a/src/Illuminate/Console/Concerns/InteractsWithIO.php b/src/Illuminate/Console/Concerns/InteractsWithIO.php index bdb594b0781f..6a7be40c54cf 100644 --- a/src/Illuminate/Console/Concerns/InteractsWithIO.php +++ b/src/Illuminate/Console/Concerns/InteractsWithIO.php @@ -15,6 +15,15 @@ trait InteractsWithIO { + /** + * The console components factory. + * + * @var \Illuminate\Console\View\Components\Factory + * + * @internal This property is not meant to be used or overwritten outside the framework. + */ + protected $components; + /** * The input interface implementation. * diff --git a/src/Illuminate/Console/ConfirmableTrait.php b/src/Illuminate/Console/ConfirmableTrait.php index 8d0d6df77808..c575dbe23d71 100644 --- a/src/Illuminate/Console/ConfirmableTrait.php +++ b/src/Illuminate/Console/ConfirmableTrait.php @@ -13,7 +13,7 @@ trait ConfirmableTrait * @param \Closure|bool|null $callback * @return bool */ - public function confirmToProceed($warning = 'Application In Production!', $callback = null) + public function confirmToProceed($warning = 'Application In Production', $callback = null) { $callback = is_null($callback) ? $this->getDefaultConfirmCallback() : $callback; @@ -24,12 +24,12 @@ public function confirmToProceed($warning = 'Application In Production!', $callb return true; } - $this->alert($warning); + $this->components->alert($warning); - $confirmed = $this->confirm('Do you really wish to run this command?'); + $confirmed = $this->components->confirm('Do you really wish to run this command?'); if (! $confirmed) { - $this->comment('Command Canceled!'); + $this->components->warn('Command canceled.'); return false; } diff --git a/src/Illuminate/Console/Contracts/NewLineAware.php b/src/Illuminate/Console/Contracts/NewLineAware.php new file mode 100644 index 000000000000..135cecba8062 --- /dev/null +++ b/src/Illuminate/Console/Contracts/NewLineAware.php @@ -0,0 +1,13 @@ +isReservedName($this->getNameInput())) { - $this->error('The name "'.$this->getNameInput().'" is reserved by PHP.'); + $this->components->error('The name "'.$this->getNameInput().'" is reserved by PHP.'); return false; } @@ -162,7 +162,7 @@ public function handle() if ((! $this->hasOption('force') || ! $this->option('force')) && $this->alreadyExists($this->getNameInput())) { - $this->error($this->type.' already exists!'); + $this->components->error($this->type.' already exists.'); return false; } @@ -174,11 +174,15 @@ public function handle() $this->files->put($path, $this->sortImports($this->buildClass($name))); - $this->info($this->type.' created successfully.'); + $info = $this->type; if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) { - $this->handleTestCreation($path); + if ($this->handleTestCreation($path)) { + $info .= ' and test'; + } } + + $this->components->info($info.' created successfully.'); } /** diff --git a/src/Illuminate/Console/OutputStyle.php b/src/Illuminate/Console/OutputStyle.php index 78083a9aba47..06eeef186b14 100644 --- a/src/Illuminate/Console/OutputStyle.php +++ b/src/Illuminate/Console/OutputStyle.php @@ -2,11 +2,12 @@ namespace Illuminate\Console; +use Illuminate\Console\Contracts\NewLineAware; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; -class OutputStyle extends SymfonyStyle +class OutputStyle extends SymfonyStyle implements NewLineAware { /** * The output instance. @@ -15,6 +16,13 @@ class OutputStyle extends SymfonyStyle */ private $output; + /** + * If the last output written wrote a new line. + * + * @var bool + */ + protected $newLineWritten = false; + /** * Create a new Console OutputStyle instance. * @@ -29,6 +37,48 @@ public function __construct(InputInterface $input, OutputInterface $output) parent::__construct($input, $output); } + /** + * {@inheritdoc} + */ + public function write(string|iterable $messages, bool $newline = false, int $options = 0) + { + $this->newLineWritten = $newline; + + parent::write($messages, $newline, $options); + } + + /** + * {@inheritdoc} + */ + public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL) + { + $this->newLineWritten = true; + + parent::writeln($messages, $type); + } + + /** + * {@inheritdoc} + */ + public function newLine(int $count = 1) + { + $this->newLineWritten = $count > 0; + + parent::newLine($count); + } + + /** + * {@inheritdoc} + */ + public function newLineWritten() + { + if ($this->output instanceof static && $this->output->newLineWritten()) { + return true; + } + + return $this->newLineWritten; + } + /** * Returns whether verbosity is quiet (-q). * diff --git a/src/Illuminate/Console/QuestionHelper.php b/src/Illuminate/Console/QuestionHelper.php new file mode 100644 index 000000000000..06d90b65fd74 --- /dev/null +++ b/src/Illuminate/Console/QuestionHelper.php @@ -0,0 +1,77 @@ +getQuestion()); + + $text = $this->ensureEndsWithPunctuation($text); + + $text = " $text"; + + $default = $question->getDefault(); + + if ($question->isMultiline()) { + $text .= sprintf(' (press %s to continue)', 'Windows' == PHP_OS_FAMILY + ? 'Ctrl+Z then Enter' + : 'Ctrl+D'); + } + + switch (true) { + case null === $default: + $text = sprintf('%s', $text); + + break; + + case $question instanceof ConfirmationQuestion: + $text = sprintf('%s (yes/no) [%s]', $text, $default ? 'yes' : 'no'); + + break; + + case $question instanceof ChoiceQuestion: + $choices = $question->getChoices(); + $text = sprintf('%s [%s]', $text, OutputFormatter::escape($choices[$default] ?? $default)); + + break; + } + + $output->writeln($text); + + if ($question instanceof ChoiceQuestion) { + foreach ($question->getChoices() as $key => $value) { + with(new TwoColumnDetail($output))->render($value, $key); + } + } + + $output->write('❯ '); + } + + /** + * Ensures the given string ends with punctuation. + * + * @param string $string + * @return string + */ + protected function ensureEndsWithPunctuation($string) + { + if (! str($string)->endsWith(['?', ':', '!', '.'])) { + return "$string:"; + } + + return $string; + } +} diff --git a/src/Illuminate/Console/Scheduling/Schedule.php b/src/Illuminate/Console/Scheduling/Schedule.php index cda3d5d1c28a..2f62bf9e81d9 100644 --- a/src/Illuminate/Console/Scheduling/Schedule.php +++ b/src/Illuminate/Console/Scheduling/Schedule.php @@ -22,11 +22,17 @@ class Schedule use Macroable; const SUNDAY = 0; + const MONDAY = 1; + const TUESDAY = 2; + const WEDNESDAY = 3; + const THURSDAY = 4; + const FRIDAY = 5; + const SATURDAY = 6; /** diff --git a/src/Illuminate/Console/Scheduling/ScheduleClearCacheCommand.php b/src/Illuminate/Console/Scheduling/ScheduleClearCacheCommand.php index 0dd9424c4bd3..3deb1c6bcfda 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleClearCacheCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleClearCacheCommand.php @@ -32,7 +32,7 @@ public function handle(Schedule $schedule) foreach ($schedule->events($this->laravel) as $event) { if ($event->mutex->exists($event)) { - $this->line('Deleting mutex for: '.$event->command); + $this->components->info(sprintf('Deleting mutex for [%s]', $event->command)); $event->mutex->forget($event); @@ -41,7 +41,7 @@ public function handle(Schedule $schedule) } if (! $mutexCleared) { - $this->info('No mutex files were found.'); + $this->components->info('No mutex files were found.'); } } } diff --git a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php index 16b9bf46dda3..cb4236a9dd10 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleListCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleListCommand.php @@ -61,7 +61,7 @@ public function handle(Schedule $schedule) $events = collect($schedule->events()); if ($events->isEmpty()) { - $this->comment('No scheduled tasks have been defined.'); + $this->components->info('No scheduled tasks have been defined.'); return; } diff --git a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php index 680c0e107b8d..067aebd05519 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php @@ -2,6 +2,7 @@ namespace Illuminate\Console\Scheduling; +use Illuminate\Console\Application; use Illuminate\Console\Command; use Illuminate\Console\Events\ScheduledTaskFailed; use Illuminate\Console\Events\ScheduledTaskFinished; @@ -9,6 +10,7 @@ use Illuminate\Console\Events\ScheduledTaskStarting; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Date; use Symfony\Component\Console\Attribute\AsCommand; use Throwable; @@ -76,6 +78,13 @@ class ScheduleRunCommand extends Command */ protected $handler; + /** + * The PHP binary used by the command. + * + * @var string + */ + protected $phpBinary; + /** * Create a new command instance. * @@ -101,6 +110,9 @@ public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHand $this->schedule = $schedule; $this->dispatcher = $dispatcher; $this->handler = $handler; + $this->phpBinary = Application::phpBinary(); + + $this->newLine(); foreach ($this->schedule->dueEvents($this->laravel) as $event) { if (! $event->filtersPass($this->laravel)) { @@ -119,7 +131,9 @@ public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHand } if (! $this->eventsRan) { - $this->info('No scheduled commands are ready to run.'); + $this->components->info('No scheduled commands are ready to run.'); + } else { + $this->newLine(); } } @@ -134,7 +148,9 @@ protected function runSingleServerEvent($event) if ($this->schedule->serverShouldRun($event, $this->startedAt)) { $this->runEvent($event); } else { - $this->line('Skipping command (has already run on another server): '.$event->getSummaryForDisplay()); + $this->components->info(sprintf( + 'Skipping [%s], as command already run on another server.', $event->getSummaryForDisplay() + )); } } @@ -146,25 +162,46 @@ protected function runSingleServerEvent($event) */ protected function runEvent($event) { - $this->line('['.date('c').'] Running scheduled command: '.$event->getSummaryForDisplay()); + $summary = $event->getSummaryForDisplay(); - $this->dispatcher->dispatch(new ScheduledTaskStarting($event)); + $command = $event instanceof CallbackEvent + ? $summary + : trim(str_replace($this->phpBinary, '', $event->command)); - $start = microtime(true); + $description = sprintf( + '%s Running [%s]%s', + Carbon::now()->format('Y-m-d H:i:s'), + $command, + $event->runInBackground ? ' in background' : '', + ); - try { - $event->run($this->laravel); + $this->components->task($description, function () use ($event) { + $this->dispatcher->dispatch(new ScheduledTaskStarting($event)); - $this->dispatcher->dispatch(new ScheduledTaskFinished( - $event, - round(microtime(true) - $start, 2) - )); + $start = microtime(true); - $this->eventsRan = true; - } catch (Throwable $e) { - $this->dispatcher->dispatch(new ScheduledTaskFailed($event, $e)); + try { + $event->run($this->laravel); + + $this->dispatcher->dispatch(new ScheduledTaskFinished( + $event, + round(microtime(true) - $start, 2) + )); + + $this->eventsRan = true; + } catch (Throwable $e) { + $this->dispatcher->dispatch(new ScheduledTaskFailed($event, $e)); + + $this->handler->report($e); + } + + return $event->exitCode == 0; + }); - $this->handler->report($e); + if (! $event instanceof CallbackEvent) { + $this->components->bulletList([ + $event->getSummaryForDisplay(), + ]); } } } diff --git a/src/Illuminate/Console/Scheduling/ScheduleTestCommand.php b/src/Illuminate/Console/Scheduling/ScheduleTestCommand.php index 2ad1c88676c8..a8a1a596b77c 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleTestCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleTestCommand.php @@ -4,7 +4,6 @@ use Illuminate\Console\Application; use Illuminate\Console\Command; -use Illuminate\Support\Carbon; use Symfony\Component\Console\Attribute\AsCommand; #[AsCommand(name: 'schedule:test')] @@ -43,6 +42,8 @@ class ScheduleTestCommand extends Command */ public function handle(Schedule $schedule) { + $phpBinary = Application::phpBinary(); + $commands = $schedule->events(); $commandNames = []; @@ -52,29 +53,47 @@ public function handle(Schedule $schedule) } if (empty($commandNames)) { - return $this->comment('No scheduled commands have been defined.'); + return $this->components->info('No scheduled commands have been defined.'); } if (! empty($name = $this->option('name'))) { - $commandBinary = Application::phpBinary().' '.Application::artisanBinary(); + $commandBinary = $phpBinary.' '.Application::artisanBinary(); $matches = array_filter($commandNames, function ($commandName) use ($commandBinary, $name) { return trim(str_replace($commandBinary, '', $commandName)) === $name; }); if (count($matches) !== 1) { - return $this->error('No matching scheduled command found.'); + $this->components->info('No matching scheduled command found.'); + + return; } $index = key($matches); } else { - $index = array_search($this->choice('Which command would you like to run?', $commandNames), $commandNames); + $index = array_search($this->components->choice('Which command would you like to run?', $commandNames), $commandNames); } $event = $commands[$index]; - $this->line('['.Carbon::now()->format('c').'] Running scheduled command: '.$event->getSummaryForDisplay()); + $summary = $event->getSummaryForDisplay(); + + $command = $event instanceof CallbackEvent + ? $summary + : trim(str_replace($phpBinary, '', $event->command)); + + $description = sprintf( + 'Running [%s]%s', + $command, + $event->runInBackground ? ' in background' : '', + ); + + $this->components->task($description, fn () => $event->run($this->laravel)); + + if (! $event instanceof CallbackEvent) { + $this->components->bulletList([$event->getSummaryForDisplay()]); + } - $event->run($this->laravel); + $this->newLine(); } } diff --git a/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php b/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php index 78d2411dcfdb..67c8a5c4fe19 100644 --- a/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php +++ b/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php @@ -42,7 +42,7 @@ class ScheduleWorkCommand extends Command */ public function handle() { - $this->info('Schedule worker started successfully.'); + $this->components->info('Running schedule tasks every minute.'); [$lastExecutionStartedAt, $keyOfLastExecutionWithOutput, $executions] = [null, null, []]; @@ -63,18 +63,10 @@ public function handle() } foreach ($executions as $key => $execution) { - $output = trim($execution->getIncrementalOutput()). - trim($execution->getIncrementalErrorOutput()); + $output = $execution->getIncrementalOutput(). + $execution->getIncrementalErrorOutput(); - if (! empty($output)) { - if ($key !== $keyOfLastExecutionWithOutput) { - $this->info(PHP_EOL.'['.date('c').'] Execution #'.($key + 1).' output:'); - - $keyOfLastExecutionWithOutput = $key; - } - - $this->output->writeln($output); - } + $this->output->write(ltrim($output, "\n")); if (! $execution->isRunning()) { unset($executions[$key]); diff --git a/src/Illuminate/Console/View/Components/Alert.php b/src/Illuminate/Console/View/Components/Alert.php new file mode 100644 index 000000000000..a975aaf8346c --- /dev/null +++ b/src/Illuminate/Console/View/Components/Alert.php @@ -0,0 +1,28 @@ +mutate($string, [ + Mutators\EnsureDynamicContentIsHighlighted::class, + Mutators\EnsurePunctuation::class, + Mutators\EnsureRelativePaths::class, + ]); + + $this->renderView('alert', [ + 'content' => $string, + ], $verbosity); + } +} diff --git a/src/Illuminate/Console/View/Components/BulletList.php b/src/Illuminate/Console/View/Components/BulletList.php new file mode 100644 index 000000000000..da3d8817960e --- /dev/null +++ b/src/Illuminate/Console/View/Components/BulletList.php @@ -0,0 +1,28 @@ + $elements + * @param int $verbosity + * @return void + */ + public function render($elements, $verbosity = OutputInterface::VERBOSITY_NORMAL) + { + $elements = $this->mutate($elements, [ + Mutators\EnsureDynamicContentIsHighlighted::class, + Mutators\EnsureNoPunctuation::class, + Mutators\EnsureRelativePaths::class, + ]); + + $this->renderView('bullet-list', [ + 'elements' => $elements, + ], $verbosity); + } +} diff --git a/src/Illuminate/Console/View/Components/Choice.php b/src/Illuminate/Console/View/Components/Choice.php new file mode 100644 index 000000000000..6a669b946e45 --- /dev/null +++ b/src/Illuminate/Console/View/Components/Choice.php @@ -0,0 +1,25 @@ + $choices + * @param mixed $default + * @return mixed + */ + public function render($question, $choices, $default = null) + { + return $this->usingQuestionHelper( + fn () => $this->output->askQuestion( + new ChoiceQuestion($question, $choices, $default) + ), + ); + } +} diff --git a/src/Illuminate/Console/View/Components/Component.php b/src/Illuminate/Console/View/Components/Component.php new file mode 100644 index 000000000000..11d95bb8f621 --- /dev/null +++ b/src/Illuminate/Console/View/Components/Component.php @@ -0,0 +1,122 @@ + + */ + protected $mutators; + + /** + * Creates a new component instance. + * + * @param \Illuminate\Console\OutputStyle $output + * @return void + */ + public function __construct($output) + { + $this->output = $output; + } + + /** + * Renders the given view. + * + * @param string $view + * @param \Illuminate\Contracts\Support\Arrayable|array $data + * @param int $verbosity + * @return void + */ + protected function renderView($view, $data, $verbosity) + { + renderUsing($this->output); + + render((string) $this->compile($view, $data), $verbosity); + } + + /** + * Compile the given view contents. + * + * @param string $view + * @param array $data + * @return void + */ + protected function compile($view, $data) + { + extract($data); + + ob_start(); + + include __DIR__."/../../resources/views/components/$view.php"; + + return tap(ob_get_contents(), function () { + ob_end_clean(); + }); + } + + /** + * Mutates the given data with the given set of mutators. + * + * @param array|string $data + * @param array $mutators + * @return array|string + */ + protected function mutate($data, $mutators) + { + foreach ($mutators as $mutator) { + if (is_iterable($data)) { + foreach ($data as $key => $value) { + $data[$key] = resolve($mutator)->__invoke($value); + } + } else { + $data = resolve($mutator)->__invoke($data); + } + } + + return $data; + } + + /** + * Eventually performs a question using the component's question helper. + * + * @param callable $callable + * @return mixed + */ + protected function usingQuestionHelper($callable) + { + $property = with(new ReflectionClass(OutputStyle::class)) + ->getParentClass() + ->getProperty('questionHelper'); + + $property->setAccessible(true); + + $currentHelper = $property->isInitialized($this->output) + ? $property->getValue($this->output) + : new SymfonyQuestionHelper(); + + $property->setValue($this->output, new QuestionHelper); + + try { + return $callable(); + } finally { + $property->setValue($this->output, $currentHelper); + } + } +} diff --git a/src/Illuminate/Console/View/Components/Confirm.php b/src/Illuminate/Console/View/Components/Confirm.php new file mode 100644 index 000000000000..1d51832e557c --- /dev/null +++ b/src/Illuminate/Console/View/Components/Confirm.php @@ -0,0 +1,20 @@ +usingQuestionHelper( + fn () => $this->output->confirm($question, $default), + ); + } +} diff --git a/src/Illuminate/Console/View/Components/Error.php b/src/Illuminate/Console/View/Components/Error.php new file mode 100644 index 000000000000..73196cc8440e --- /dev/null +++ b/src/Illuminate/Console/View/Components/Error.php @@ -0,0 +1,20 @@ +output))->render('error', $string, $verbosity); + } +} diff --git a/src/Illuminate/Console/View/Components/Factory.php b/src/Illuminate/Console/View/Components/Factory.php new file mode 100644 index 000000000000..54cacba15791 --- /dev/null +++ b/src/Illuminate/Console/View/Components/Factory.php @@ -0,0 +1,46 @@ +output = $output; + } + + /** + * Dynamically handle calls into the component instance. + * + * @param string $method + * @param array $parameters + * @return mixed + * + * @throws \InvalidArgumentException + */ + public function __call($method, $parameters) + { + $component = '\Illuminate\Console\View\Components\\'.ucfirst($method); + + throw_unless(class_exists($component), new InvalidArgumentException(sprintf( + 'Console component [%s] not found.', $method + ))); + + return with(new $component($this->output))->render(...$parameters); + } +} diff --git a/src/Illuminate/Console/View/Components/Info.php b/src/Illuminate/Console/View/Components/Info.php new file mode 100644 index 000000000000..765142246fed --- /dev/null +++ b/src/Illuminate/Console/View/Components/Info.php @@ -0,0 +1,20 @@ +output))->render('info', $string, $verbosity); + } +} diff --git a/src/Illuminate/Console/View/Components/Line.php b/src/Illuminate/Console/View/Components/Line.php new file mode 100644 index 000000000000..5c701ee5aa51 --- /dev/null +++ b/src/Illuminate/Console/View/Components/Line.php @@ -0,0 +1,54 @@ +> + */ + protected static $styles = [ + 'info' => [ + 'bgColor' => 'blue', + 'fgColor' => 'white', + 'title' => 'info', + ], + 'warn' => [ + 'bgColor' => 'yellow', + 'fgColor' => 'black', + 'title' => 'warn', + ], + 'error' => [ + 'bgColor' => 'red', + 'fgColor' => 'white', + 'title' => 'error', + ], + ]; + + /** + * Renders the component using the given arguments. + * + * @param string $style + * @param string $string + * @param int $verbosity + * @return void + */ + public function render($style, $string, $verbosity = OutputInterface::VERBOSITY_NORMAL) + { + $string = $this->mutate($string, [ + Mutators\EnsureDynamicContentIsHighlighted::class, + Mutators\EnsurePunctuation::class, + Mutators\EnsureRelativePaths::class, + ]); + + $this->renderView('line', array_merge(static::$styles[$style], [ + 'marginTop' => ($this->output instanceof NewLineAware && $this->output->newLineWritten()) ? 0 : 1, + 'content' => $string, + ]), $verbosity); + } +} diff --git a/src/Illuminate/Console/View/Components/Mutators/EnsureDynamicContentIsHighlighted.php b/src/Illuminate/Console/View/Components/Mutators/EnsureDynamicContentIsHighlighted.php new file mode 100644 index 000000000000..16575b40c934 --- /dev/null +++ b/src/Illuminate/Console/View/Components/Mutators/EnsureDynamicContentIsHighlighted.php @@ -0,0 +1,17 @@ +[$1]', (string) $string); + } +} diff --git a/src/Illuminate/Console/View/Components/Mutators/EnsureNoPunctuation.php b/src/Illuminate/Console/View/Components/Mutators/EnsureNoPunctuation.php new file mode 100644 index 000000000000..5f349362668e --- /dev/null +++ b/src/Illuminate/Console/View/Components/Mutators/EnsureNoPunctuation.php @@ -0,0 +1,21 @@ +endsWith(['.', '?', '!', ':'])) { + return substr_replace($string, '', -1); + } + + return $string; + } +} diff --git a/src/Illuminate/Console/View/Components/Mutators/EnsurePunctuation.php b/src/Illuminate/Console/View/Components/Mutators/EnsurePunctuation.php new file mode 100644 index 000000000000..c99fecffa9ec --- /dev/null +++ b/src/Illuminate/Console/View/Components/Mutators/EnsurePunctuation.php @@ -0,0 +1,21 @@ +endsWith(['.', '?', '!', ':'])) { + return "$string."; + } + + return $string; + } +} diff --git a/src/Illuminate/Console/View/Components/Mutators/EnsureRelativePaths.php b/src/Illuminate/Console/View/Components/Mutators/EnsureRelativePaths.php new file mode 100644 index 000000000000..50f42c6fffd3 --- /dev/null +++ b/src/Illuminate/Console/View/Components/Mutators/EnsureRelativePaths.php @@ -0,0 +1,21 @@ +has('path.base')) { + $string = str_replace(base_path().'/', '', $string); + } + + return $string; + } +} diff --git a/src/Illuminate/Console/View/Components/Task.php b/src/Illuminate/Console/View/Components/Task.php new file mode 100644 index 000000000000..001bbb3a940b --- /dev/null +++ b/src/Illuminate/Console/View/Components/Task.php @@ -0,0 +1,57 @@ +mutate($description, [ + Mutators\EnsureDynamicContentIsHighlighted::class, + Mutators\EnsureNoPunctuation::class, + Mutators\EnsureRelativePaths::class, + ]); + + $descriptionWidth = mb_strlen(preg_replace("/\<[\w=#\/\;,:.&,%?]+\>|\\e\[\d+m/", '$1', $description) ?? ''); + + $this->output->write(" $description ", false, $verbosity); + + $startTime = microtime(true); + + $result = false; + + try { + $result = ($task ?: fn () => true)(); + } catch (Throwable $e) { + throw $e; + } finally { + $runTime = $task + ? (' '.number_format((microtime(true) - $startTime) * 1000, 2).'ms') + : ''; + + $runTimeWidth = mb_strlen($runTime); + $width = min(terminal()->width(), 150); + $dots = max($width - $descriptionWidth - $runTimeWidth - 10, 0); + + $this->output->write(str_repeat('.', $dots), false, $verbosity); + $this->output->write("$runTime", false, $verbosity); + + $this->output->writeln( + $result !== false ? ' DONE' : ' FAIL', + $verbosity, + ); + } + } +} diff --git a/src/Illuminate/Console/View/Components/TwoColumnDetail.php b/src/Illuminate/Console/View/Components/TwoColumnDetail.php new file mode 100644 index 000000000000..1ffa089373ed --- /dev/null +++ b/src/Illuminate/Console/View/Components/TwoColumnDetail.php @@ -0,0 +1,36 @@ +mutate($first, [ + Mutators\EnsureDynamicContentIsHighlighted::class, + Mutators\EnsureNoPunctuation::class, + Mutators\EnsureRelativePaths::class, + ]); + + $second = $this->mutate($second, [ + Mutators\EnsureDynamicContentIsHighlighted::class, + Mutators\EnsureNoPunctuation::class, + Mutators\EnsureRelativePaths::class, + ]); + + $this->renderView('two-column-detail', [ + 'first' => $first, + 'second' => $second, + ], $verbosity); + } +} diff --git a/src/Illuminate/Console/View/Components/Warn.php b/src/Illuminate/Console/View/Components/Warn.php new file mode 100644 index 000000000000..20adb1f272b7 --- /dev/null +++ b/src/Illuminate/Console/View/Components/Warn.php @@ -0,0 +1,21 @@ +output)) + ->render('warn', $string, $verbosity); + } +} diff --git a/src/Illuminate/Console/composer.json b/src/Illuminate/Console/composer.json index 5a768863bc01..948a6fc16db4 100755 --- a/src/Illuminate/Console/composer.json +++ b/src/Illuminate/Console/composer.json @@ -19,6 +19,8 @@ "illuminate/contracts": "^9.0", "illuminate/macroable": "^9.0", "illuminate/support": "^9.0", + "illuminate/view": "^9.0", + "nunomaduro/termwind": "^1.13", "symfony/console": "^6.0", "symfony/process": "^6.0" }, diff --git a/src/Illuminate/Console/resources/views/components/alert.php b/src/Illuminate/Console/resources/views/components/alert.php new file mode 100644 index 000000000000..bddcb21306d4 --- /dev/null +++ b/src/Illuminate/Console/resources/views/components/alert.php @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/Illuminate/Console/resources/views/components/bullet-list.php b/src/Illuminate/Console/resources/views/components/bullet-list.php new file mode 100644 index 000000000000..a016a9108121 --- /dev/null +++ b/src/Illuminate/Console/resources/views/components/bullet-list.php @@ -0,0 +1,7 @@ +
+ +
+ ⇂ +
+ +
diff --git a/src/Illuminate/Console/resources/views/components/line.php b/src/Illuminate/Console/resources/views/components/line.php new file mode 100644 index 000000000000..a6c38cfab00a --- /dev/null +++ b/src/Illuminate/Console/resources/views/components/line.php @@ -0,0 +1,8 @@ +
+ + + + +
diff --git a/src/Illuminate/Console/resources/views/components/two-column-detail.php b/src/Illuminate/Console/resources/views/components/two-column-detail.php new file mode 100644 index 000000000000..1aeed496f8ae --- /dev/null +++ b/src/Illuminate/Console/resources/views/components/two-column-detail.php @@ -0,0 +1,11 @@ +
+ + + + + + + + + +
diff --git a/src/Illuminate/Database/Console/DumpCommand.php b/src/Illuminate/Database/Console/DumpCommand.php index 71171f32b744..50a6ad076454 100644 --- a/src/Illuminate/Database/Console/DumpCommand.php +++ b/src/Illuminate/Database/Console/DumpCommand.php @@ -59,15 +59,17 @@ public function handle(ConnectionResolverInterface $connections, Dispatcher $dis $dispatcher->dispatch(new SchemaDumped($connection, $path)); - $this->info('Database schema dumped successfully.'); + $info = 'Database schema dumped'; if ($this->option('prune')) { (new Filesystem)->deleteDirectory( database_path('migrations'), $preserve = false ); - $this->info('Migrations pruned successfully.'); + $info .= ' and pruned'; } + + $this->components->info($info.' successfully.'); } /** diff --git a/src/Illuminate/Database/Console/Migrations/FreshCommand.php b/src/Illuminate/Database/Console/Migrations/FreshCommand.php index 7bfba0d78821..d3d8bbe534d8 100644 --- a/src/Illuminate/Database/Console/Migrations/FreshCommand.php +++ b/src/Illuminate/Database/Console/Migrations/FreshCommand.php @@ -39,12 +39,16 @@ public function handle() $database = $this->input->getOption('database'); - $this->call('db:wipe', array_filter([ + $this->newLine(); + + $this->components->task('Dropping all tables', fn () => $this->callSilent('db:wipe', array_filter([ '--database' => $database, '--drop-views' => $this->option('drop-views'), '--drop-types' => $this->option('drop-types'), '--force' => true, - ])); + ])) == 0); + + $this->newLine(); $this->call('migrate', array_filter([ '--database' => $database, @@ -86,6 +90,8 @@ protected function needsSeeding() */ protected function runSeeder($database) { + $this->newLine(); + $this->call('db:seed', array_filter([ '--database' => $database, '--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder', diff --git a/src/Illuminate/Database/Console/Migrations/InstallCommand.php b/src/Illuminate/Database/Console/Migrations/InstallCommand.php index d69c2ab6b5aa..901a83babb30 100755 --- a/src/Illuminate/Database/Console/Migrations/InstallCommand.php +++ b/src/Illuminate/Database/Console/Migrations/InstallCommand.php @@ -53,7 +53,7 @@ public function handle() $this->repository->createRepository(); - $this->info('Migration table created successfully.'); + $this->components->info('Migration table created successfully.'); } /** diff --git a/src/Illuminate/Database/Console/Migrations/MigrateCommand.php b/src/Illuminate/Database/Console/Migrations/MigrateCommand.php index ea379e3f6d28..444876d85cc1 100755 --- a/src/Illuminate/Database/Console/Migrations/MigrateCommand.php +++ b/src/Illuminate/Database/Console/Migrations/MigrateCommand.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Console\Migrations; use Illuminate\Console\ConfirmableTrait; +use Illuminate\Console\View\Components\Task; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Events\SchemaLoaded; use Illuminate\Database\Migrations\Migrator; @@ -80,7 +81,7 @@ public function handle() // Next, we will check to see if a path option has been defined. If it has // we will use the path relative to the root of this installation folder // so that migrations may be run for any path within the applications. - $this->migrator->setOutput($this->output) + $migrations = $this->migrator->setOutput($this->output) ->run($this->getMigrationPaths(), [ 'pretend' => $this->option('pretend'), 'step' => $this->option('step'), @@ -90,6 +91,10 @@ public function handle() // seed task to re-populate the database, which is convenient when adding // a migration and a seed at the same time, as it is only this command. if ($this->option('seed') && ! $this->option('pretend')) { + if (! empty($migrations)) { + $this->newLine(); + } + $this->call('db:seed', [ '--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder', '--force' => true, @@ -108,9 +113,15 @@ public function handle() protected function prepareDatabase() { if (! $this->migrator->repositoryExists()) { - $this->call('migrate:install', array_filter([ - '--database' => $this->option('database'), - ])); + $this->components->info('Preparing database.'); + + $this->components->task('Creating migration table', function () { + return $this->callSilent('migrate:install', array_filter([ + '--database' => $this->option('database'), + ])) == 0; + }); + + $this->newLine(); } if (! $this->migrator->hasRunAnyMigrations() && ! $this->option('pretend')) { @@ -135,20 +146,20 @@ protected function loadSchemaState() return; } - $this->line('Loading stored database schema: '.$path); + $this->components->info('Loading stored database schemas.'); - $startTime = microtime(true); + $this->components->task($path, function () use ($connection, $path) { + // Since the schema file will create the "migrations" table and reload it to its + // proper state, we need to delete it here so we don't get an error that this + // table already exists when the stored database schema file gets executed. + $this->migrator->deleteRepository(); - // Since the schema file will create the "migrations" table and reload it to its - // proper state, we need to delete it here so we don't get an error that this - // table already exists when the stored database schema file gets executed. - $this->migrator->deleteRepository(); - - $connection->getSchemaState()->handleOutputUsing(function ($type, $buffer) { - $this->output->write($buffer); - })->load($path); + $connection->getSchemaState()->handleOutputUsing(function ($type, $buffer) { + $this->output->write($buffer); + })->load($path); + }); - $runTime = number_format((microtime(true) - $startTime) * 1000, 2); + $this->newLine(); // Finally, we will fire an event that this schema has been loaded so developers // can perform any post schema load tasks that are necessary in listeners for @@ -156,8 +167,6 @@ protected function loadSchemaState() $this->dispatcher->dispatch( new SchemaLoaded($connection, $path) ); - - $this->line('Loaded stored database schema. ('.$runTime.'ms)'); } /** diff --git a/src/Illuminate/Database/Console/Migrations/MigrateMakeCommand.php b/src/Illuminate/Database/Console/Migrations/MigrateMakeCommand.php index 95c3a206e54a..c1f070e7ec85 100644 --- a/src/Illuminate/Database/Console/Migrations/MigrateMakeCommand.php +++ b/src/Illuminate/Database/Console/Migrations/MigrateMakeCommand.php @@ -114,7 +114,7 @@ protected function writeMigration($name, $table, $create) $file = pathinfo($file, PATHINFO_FILENAME); } - $this->line("Created Migration: {$file}"); + $this->components->info(sprintf('Created migration [%s].', $file)); } /** diff --git a/src/Illuminate/Database/Console/Migrations/RefreshCommand.php b/src/Illuminate/Database/Console/Migrations/RefreshCommand.php index 2073cd9977e6..aff48ace944c 100755 --- a/src/Illuminate/Database/Console/Migrations/RefreshCommand.php +++ b/src/Illuminate/Database/Console/Migrations/RefreshCommand.php @@ -132,6 +132,8 @@ protected function needsSeeding() */ protected function runSeeder($database) { + $this->newLine(); + $this->call('db:seed', array_filter([ '--database' => $database, '--class' => $this->option('seeder') ?: 'Database\\Seeders\\DatabaseSeeder', diff --git a/src/Illuminate/Database/Console/Migrations/ResetCommand.php b/src/Illuminate/Database/Console/Migrations/ResetCommand.php index 1f2babbc8d08..c5952fa0532a 100755 --- a/src/Illuminate/Database/Console/Migrations/ResetCommand.php +++ b/src/Illuminate/Database/Console/Migrations/ResetCommand.php @@ -60,7 +60,7 @@ public function handle() // start trying to rollback and re-run all of the migrations. If it's not // present we'll just bail out with an info message for the developers. if (! $this->migrator->repositoryExists()) { - return $this->comment('Migration table not found.'); + return $this->components->warn('Migration table not found.'); } $this->migrator->setOutput($this->output)->reset( diff --git a/src/Illuminate/Database/Console/Migrations/StatusCommand.php b/src/Illuminate/Database/Console/Migrations/StatusCommand.php index f57fe53a507f..60ad9dc19a96 100644 --- a/src/Illuminate/Database/Console/Migrations/StatusCommand.php +++ b/src/Illuminate/Database/Console/Migrations/StatusCommand.php @@ -51,7 +51,7 @@ public function handle() { return $this->migrator->usingConnection($this->option('database'), function () { if (! $this->migrator->repositoryExists()) { - $this->error('Migration table not found.'); + $this->components->error('Migration table not found.'); return 1; } @@ -61,9 +61,17 @@ public function handle() $batches = $this->migrator->getRepository()->getMigrationBatches(); if (count($migrations = $this->getStatusFor($ran, $batches)) > 0) { - $this->table(['Ran?', 'Migration', 'Batch'], $migrations); + $this->newLine(); + + $this->components->twoColumnDetail('Migration name', 'Batch / Status'); + + $migrations->each( + fn ($migration) => $this->components->twoColumnDetail($migration[0], $migration[1]) + ); + + $this->newLine(); } else { - $this->error('No migrations found'); + $this->components->info('No migrations found'); } }); } @@ -81,9 +89,15 @@ protected function getStatusFor(array $ran, array $batches) ->map(function ($migration) use ($ran, $batches) { $migrationName = $this->migrator->getMigrationName($migration); - return in_array($migrationName, $ran) - ? ['Yes', $migrationName, $batches[$migrationName]] - : ['No', $migrationName]; + $status = in_array($migrationName, $ran) + ? 'Ran' + : 'Pending'; + + if (in_array($migrationName, $ran)) { + $status = '['.$batches[$migrationName].'] '.$status; + } + + return [$migrationName, $status]; }); } diff --git a/src/Illuminate/Database/Console/PruneCommand.php b/src/Illuminate/Database/Console/PruneCommand.php index b69aa86849d7..617a184ef142 100644 --- a/src/Illuminate/Database/Console/PruneCommand.php +++ b/src/Illuminate/Database/Console/PruneCommand.php @@ -43,7 +43,7 @@ public function handle(Dispatcher $events) $models = $this->models(); if ($models->isEmpty()) { - $this->info('No prunable models found.'); + $this->components->info('No prunable models found.'); return; } @@ -56,8 +56,18 @@ public function handle(Dispatcher $events) return; } - $events->listen(ModelsPruned::class, function ($event) { - $this->info("{$event->count} [{$event->model}] records have been pruned."); + $prunning = []; + + $events->listen(ModelsPruned::class, function ($event) use (&$prunning) { + if (! in_array($event->model, $prunning)) { + $prunning[] = $event->model; + + $this->newLine(); + + $this->components->info(sprintf('Prunning [%s] records.', $event->model)); + } + + $this->components->twoColumnDetail($event->model, "{$event->count} records"); }); $models->each(function ($model) { @@ -72,7 +82,7 @@ public function handle(Dispatcher $events) : 0; if ($total == 0) { - $this->info("No prunable [$model] records found."); + $this->components->info("No prunable [$model] records found."); } }); @@ -157,9 +167,9 @@ protected function pretendToPrune($model) })->count(); if ($count === 0) { - $this->info("No prunable [$model] records found."); + $this->components->info("No prunable [$model] records found."); } else { - $this->info("{$count} [{$model}] records will be pruned."); + $this->components->info("{$count} [{$model}] records will be pruned."); } } } diff --git a/src/Illuminate/Database/Console/Seeds/SeedCommand.php b/src/Illuminate/Database/Console/Seeds/SeedCommand.php index f64af58354c3..235958648925 100644 --- a/src/Illuminate/Database/Console/Seeds/SeedCommand.php +++ b/src/Illuminate/Database/Console/Seeds/SeedCommand.php @@ -71,6 +71,8 @@ public function handle() return 1; } + $this->components->info('Seeding database.'); + $previousConnection = $this->resolver->getDefaultConnection(); $this->resolver->setDefaultConnection($this->getDatabase()); @@ -83,8 +85,6 @@ public function handle() $this->resolver->setDefaultConnection($previousConnection); } - $this->info('Database seeding completed successfully.'); - return 0; } diff --git a/src/Illuminate/Database/Console/WipeCommand.php b/src/Illuminate/Database/Console/WipeCommand.php index 0227782fa0c0..cb269229f9d5 100644 --- a/src/Illuminate/Database/Console/WipeCommand.php +++ b/src/Illuminate/Database/Console/WipeCommand.php @@ -53,17 +53,17 @@ public function handle() if ($this->option('drop-views')) { $this->dropAllViews($database); - $this->info('Dropped all views successfully.'); + $this->components->info('Dropped all views successfully.'); } $this->dropAllTables($database); - $this->info('Dropped all tables successfully.'); + $this->components->info('Dropped all tables successfully.'); if ($this->option('drop-types')) { $this->dropAllTypes($database); - $this->info('Dropped all types successfully.'); + $this->components->info('Dropped all types successfully.'); } return 0; diff --git a/src/Illuminate/Database/Migrations/Migrator.php b/src/Illuminate/Database/Migrations/Migrator.php index 361747c6e85d..e124186bed87 100755 --- a/src/Illuminate/Database/Migrations/Migrator.php +++ b/src/Illuminate/Database/Migrations/Migrator.php @@ -3,6 +3,11 @@ namespace Illuminate\Database\Migrations; use Doctrine\DBAL\Schema\SchemaException; +use Illuminate\Console\View\Components\BulletList; +use Illuminate\Console\View\Components\Error; +use Illuminate\Console\View\Components\Info; +use Illuminate\Console\View\Components\Task; +use Illuminate\Console\View\Components\TwoColumnDetail; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\ConnectionResolverInterface as Resolver; use Illuminate\Database\Events\MigrationEnded; @@ -144,7 +149,7 @@ public function runPending(array $migrations, array $options = []) if (count($migrations) === 0) { $this->fireMigrationEvent(new NoPendingMigrations('up')); - $this->note('Nothing to migrate.'); + $this->write(Info::class, 'Nothing to migrate'); return; } @@ -160,6 +165,8 @@ public function runPending(array $migrations, array $options = []) $this->fireMigrationEvent(new MigrationsStarted('up')); + $this->write(Info::class, 'Running migrations.'); + // Once we have the array of migrations, we will spin through them and run the // migrations "up" so the changes are made to the databases. We'll then log // that the migration was run so we don't repeat it next time we execute. @@ -195,20 +202,12 @@ protected function runUp($file, $batch, $pretend) return $this->pretendToRun($migration, 'up'); } - $this->note("Migrating: {$name}"); - - $startTime = microtime(true); - - $this->runMigration($migration, 'up'); - - $runTime = number_format((microtime(true) - $startTime) * 1000, 2); + $this->write(Task::class, $name, fn () => $this->runMigration($migration, 'up')); // Once we have run a migrations class, we will log that it was run in this // repository so that we don't try to run it next time we do a migration // in the application. A migration repository keeps the migrate order. $this->repository->log($name, $batch); - - $this->note("Migrated: {$name} ({$runTime}ms)"); } /** @@ -228,7 +227,7 @@ public function rollback($paths = [], array $options = []) if (count($migrations) === 0) { $this->fireMigrationEvent(new NoPendingMigrations('down')); - $this->note('Nothing to rollback.'); + $this->write(Info::class, 'Nothing to rollback.'); return []; } @@ -267,6 +266,8 @@ protected function rollbackMigrations(array $migrations, $paths, array $options) $this->fireMigrationEvent(new MigrationsStarted('down')); + $this->write(Info::class, 'Rollbacking migrations.'); + // Next we will run through all of the migrations and call the "down" method // which will reverse each migration in order. This getLast method on the // repository already returns these migration's names in reverse order. @@ -274,7 +275,7 @@ protected function rollbackMigrations(array $migrations, $paths, array $options) $migration = (object) $migration; if (! $file = Arr::get($files, $migration->migration)) { - $this->note("Migration not found: {$migration->migration}"); + $this->write(TwoColumnDetail::class, $migration->migration, 'Migration not found'); continue; } @@ -307,12 +308,16 @@ public function reset($paths = [], $pretend = false) $migrations = array_reverse($this->repository->getRan()); if (count($migrations) === 0) { - $this->note('Nothing to rollback.'); + $this->write(Info::class, 'Nothing to rollback.'); return []; } - return $this->resetMigrations($migrations, $paths, $pretend); + return tap($this->resetMigrations($migrations, $paths, $pretend), function () { + if ($this->output) { + $this->output->writeln(''); + } + }); } /** @@ -354,24 +359,16 @@ protected function runDown($file, $migration, $pretend) $name = $this->getMigrationName($file); - $this->note("Rolling back: {$name}"); - if ($pretend) { return $this->pretendToRun($instance, 'down'); } - $startTime = microtime(true); - - $this->runMigration($instance, 'down'); - - $runTime = number_format((microtime(true) - $startTime) * 1000, 2); + $this->write(Task::class, $name, fn () => $this->runMigration($instance, 'down')); // Once we have successfully run the migration "down" we will remove it from // the migration repository so it will be considered to have not been run // by the application then will be able to fire by any later operation. $this->repository->delete($migration); - - $this->note("Rolled back: {$name} ({$runTime}ms)"); } /** @@ -413,21 +410,25 @@ protected function runMigration($migration, $method) protected function pretendToRun($migration, $method) { try { - foreach ($this->getQueries($migration, $method) as $query) { - $name = get_class($migration); - - $reflectionClass = new ReflectionClass($migration); + $name = get_class($migration); - if ($reflectionClass->isAnonymous()) { - $name = $this->getMigrationName($reflectionClass->getFileName()); - } + $reflectionClass = new ReflectionClass($migration); - $this->note("{$name}: {$query['query']}"); + if ($reflectionClass->isAnonymous()) { + $name = $this->getMigrationName($reflectionClass->getFileName()); } + + $this->write(TwoColumnDetail::class, $name); + $this->write(BulletList::class, collect($this->getQueries($migration, $method))->map(function ($query) { + return $query['query']; + })); } catch (SchemaException $e) { $name = get_class($migration); - $this->note("{$name}: failed to dump queries. This may be due to changing database columns using Doctrine, which is not supported while pretending to run migrations."); + $this->write(Error::class, sprintf( + '[%s] failed to dump queries. This may be due to changing database columns using Doctrine, which is not supported while pretending to run migrations.', + $name, + )); } } @@ -717,15 +718,16 @@ public function setOutput(OutputInterface $output) } /** - * Write a note to the console's output. + * Write to the console's output. * - * @param string $message + * @param string $component + * @param array|string $arguments * @return void */ - protected function note($message) + protected function write($component, ...$arguments) { if ($this->output) { - $this->output->writeln($message); + with(new $component($this->output))->render(...$arguments); } } diff --git a/src/Illuminate/Database/Seeder.php b/src/Illuminate/Database/Seeder.php index 1a7a12e1914d..a702609936fd 100755 --- a/src/Illuminate/Database/Seeder.php +++ b/src/Illuminate/Database/Seeder.php @@ -3,6 +3,7 @@ namespace Illuminate\Database; use Illuminate\Console\Command; +use Illuminate\Console\View\Components\Task; use Illuminate\Container\Container; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Support\Arr; @@ -48,18 +49,13 @@ public function call($class, $silent = false, array $parameters = []) $name = get_class($seeder); - if ($silent === false && isset($this->command)) { - $this->command->getOutput()->writeln("Seeding: {$name}"); - } - - $startTime = microtime(true); - - $seeder->__invoke($parameters); - - $runTime = number_format((microtime(true) - $startTime) * 1000, 2); - - if ($silent === false && isset($this->command)) { - $this->command->getOutput()->writeln("Seeded: {$name} ({$runTime}ms)"); + if ($silent || ! isset($this->command)) { + $seeder->__invoke($parameters); + } else { + with(new Task($this->command->getOutput()))->render( + $name, + fn () => $seeder->__invoke($parameters), + ); } static::$called[] = $class; diff --git a/src/Illuminate/Foundation/Console/ClearCompiledCommand.php b/src/Illuminate/Foundation/Console/ClearCompiledCommand.php index ec355e859d09..20375ded17a6 100644 --- a/src/Illuminate/Foundation/Console/ClearCompiledCommand.php +++ b/src/Illuminate/Foundation/Console/ClearCompiledCommand.php @@ -48,6 +48,6 @@ public function handle() @unlink($packagesPath); } - $this->info('Compiled services and packages files removed successfully.'); + $this->components->info('Compiled services and packages files removed successfully.'); } } diff --git a/src/Illuminate/Foundation/Console/ComponentMakeCommand.php b/src/Illuminate/Foundation/Console/ComponentMakeCommand.php index 00bb517d1eeb..6134d9ae1d13 100644 --- a/src/Illuminate/Foundation/Console/ComponentMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ComponentMakeCommand.php @@ -52,7 +52,7 @@ public function handle() { if ($this->option('view')) { $this->writeView(function () { - $this->info($this->type.' created successfully.'); + $this->components->info($this->type.' created successfully.'); }); return; @@ -84,7 +84,7 @@ protected function writeView($onSuccess = null) } if ($this->files->exists($path) && ! $this->option('force')) { - $this->error('View already exists!'); + $this->components->error('View already exists.'); return; } diff --git a/src/Illuminate/Foundation/Console/ConfigCacheCommand.php b/src/Illuminate/Foundation/Console/ConfigCacheCommand.php index 17545b41448e..18eec46c0f5c 100644 --- a/src/Illuminate/Foundation/Console/ConfigCacheCommand.php +++ b/src/Illuminate/Foundation/Console/ConfigCacheCommand.php @@ -66,7 +66,7 @@ public function __construct(Filesystem $files) */ public function handle() { - $this->call('config:clear'); + $this->callSilent('config:clear'); $config = $this->getFreshConfiguration(); @@ -84,7 +84,7 @@ public function handle() throw new LogicException('Your configuration files are not serializable.', 0, $e); } - $this->info('Configuration cached successfully.'); + $this->components->info('Configuration cached successfully.'); } /** diff --git a/src/Illuminate/Foundation/Console/ConfigClearCommand.php b/src/Illuminate/Foundation/Console/ConfigClearCommand.php index cb24bac671c8..3e07e9be9a4a 100644 --- a/src/Illuminate/Foundation/Console/ConfigClearCommand.php +++ b/src/Illuminate/Foundation/Console/ConfigClearCommand.php @@ -63,6 +63,6 @@ public function handle() { $this->files->delete($this->laravel->getCachedConfigPath()); - $this->info('Configuration cache cleared successfully.'); + $this->components->info('Configuration cache cleared successfully.'); } } diff --git a/src/Illuminate/Foundation/Console/DownCommand.php b/src/Illuminate/Foundation/Console/DownCommand.php index 3777ae6a6131..ac7392fee800 100644 --- a/src/Illuminate/Foundation/Console/DownCommand.php +++ b/src/Illuminate/Foundation/Console/DownCommand.php @@ -52,7 +52,7 @@ public function handle() { try { if ($this->laravel->maintenanceMode()->active()) { - $this->comment('Application is already down.'); + $this->components->info('Application is already down.'); return 0; } @@ -66,11 +66,12 @@ public function handle() $this->laravel->get('events')->dispatch(MaintenanceModeEnabled::class); - $this->comment('Application is now in maintenance mode.'); + $this->components->info('Application is now in maintenance mode.'); } catch (Exception $e) { - $this->error('Failed to enter maintenance mode.'); - - $this->error($e->getMessage()); + $this->components->error(sprintf( + 'Failed to enter maintenance mode: %s.', + $e->getMessage(), + )); return 1; } diff --git a/src/Illuminate/Foundation/Console/EnvironmentCommand.php b/src/Illuminate/Foundation/Console/EnvironmentCommand.php index 32e99ad8a74d..ba74ed987efc 100644 --- a/src/Illuminate/Foundation/Console/EnvironmentCommand.php +++ b/src/Illuminate/Foundation/Console/EnvironmentCommand.php @@ -40,6 +40,9 @@ class EnvironmentCommand extends Command */ public function handle() { - $this->line('Current application environment: '.$this->laravel['env'].''); + $this->components->info(sprintf( + 'The application environment is [%s].', + $this->laravel['env'], + )); } } diff --git a/src/Illuminate/Foundation/Console/EventCacheCommand.php b/src/Illuminate/Foundation/Console/EventCacheCommand.php index 9590e5b57155..df42fbfd1d65 100644 --- a/src/Illuminate/Foundation/Console/EventCacheCommand.php +++ b/src/Illuminate/Foundation/Console/EventCacheCommand.php @@ -41,14 +41,14 @@ class EventCacheCommand extends Command */ public function handle() { - $this->call('event:clear'); + $this->callSilent('event:clear'); file_put_contents( $this->laravel->getCachedEventsPath(), 'getEvents(), true).';' ); - $this->info('Events cached successfully.'); + $this->components->info('Events cached successfully.'); } /** diff --git a/src/Illuminate/Foundation/Console/EventClearCommand.php b/src/Illuminate/Foundation/Console/EventClearCommand.php index 83de371b9362..a5c8ed1937bb 100644 --- a/src/Illuminate/Foundation/Console/EventClearCommand.php +++ b/src/Illuminate/Foundation/Console/EventClearCommand.php @@ -65,6 +65,6 @@ public function handle() { $this->files->delete($this->laravel->getCachedEventsPath()); - $this->info('Cached events cleared successfully.'); + $this->components->info('Cached events cleared successfully.'); } } diff --git a/src/Illuminate/Foundation/Console/EventGenerateCommand.php b/src/Illuminate/Foundation/Console/EventGenerateCommand.php index 307141f7d366..b27e9dbb0c82 100644 --- a/src/Illuminate/Foundation/Console/EventGenerateCommand.php +++ b/src/Illuminate/Foundation/Console/EventGenerateCommand.php @@ -49,7 +49,7 @@ public function handle() } } - $this->info('Events and listeners generated successfully.'); + $this->components->info('Events and listeners generated successfully.'); } /** diff --git a/src/Illuminate/Foundation/Console/EventListCommand.php b/src/Illuminate/Foundation/Console/EventListCommand.php index 8fb109bb1b54..1e0e11b877c8 100644 --- a/src/Illuminate/Foundation/Console/EventListCommand.php +++ b/src/Illuminate/Foundation/Console/EventListCommand.php @@ -54,17 +54,19 @@ public function handle() $events = $this->getEvents()->sortKeys(); if ($events->isEmpty()) { - $this->comment("Your application doesn't have any events matching the given criteria."); + $this->components->info("Your application doesn't have any events matching the given criteria."); return; } - $this->line( - $events->map(fn ($listeners, $event) => [ - sprintf(' %s', $this->appendEventInterfaces($event)), - collect($listeners)->map(fn ($listener) => sprintf(' ⇂ %s', $listener)), - ])->flatten()->filter()->prepend('')->push('')->toArray() - ); + $this->newLine(); + + $events->each(function ($listeners, $event) { + $this->components->twoColumnDetail($this->appendEventInterfaces($event)); + $this->components->bulletList($listeners); + }); + + $this->newLine(); } /** diff --git a/src/Illuminate/Foundation/Console/KeyGenerateCommand.php b/src/Illuminate/Foundation/Console/KeyGenerateCommand.php index 48241c4042f1..afd3eebddbcc 100644 --- a/src/Illuminate/Foundation/Console/KeyGenerateCommand.php +++ b/src/Illuminate/Foundation/Console/KeyGenerateCommand.php @@ -61,7 +61,7 @@ public function handle() $this->laravel['config']['app.key'] = $key; - $this->info('Application key set successfully.'); + $this->components->info('Application key set successfully.'); } /** diff --git a/src/Illuminate/Foundation/Console/OptimizeClearCommand.php b/src/Illuminate/Foundation/Console/OptimizeClearCommand.php index 1e12960c10d6..d3b039ebb36b 100644 --- a/src/Illuminate/Foundation/Console/OptimizeClearCommand.php +++ b/src/Illuminate/Foundation/Console/OptimizeClearCommand.php @@ -40,13 +40,17 @@ class OptimizeClearCommand extends Command */ public function handle() { - $this->call('event:clear'); - $this->call('view:clear'); - $this->call('cache:clear'); - $this->call('route:clear'); - $this->call('config:clear'); - $this->call('clear-compiled'); - - $this->info('Caches cleared successfully.'); + $this->components->info('Clearing cached bootstrap files.'); + + collect([ + 'events' => fn () => $this->callSilent('event:clear') == 0, + 'views' => fn () => $this->callSilent('view:clear') == 0, + 'cache' => fn () => $this->callSilent('cache:clear') == 0, + 'route' => fn () => $this->callSilent('route:clear') == 0, + 'config' => fn () => $this->callSilent('config:clear') == 0, + 'compiled' => fn () => $this->callSilent('clear-compiled') == 0, + ])->each(fn ($task, $description) => $this->components->task($description, $task)); + + $this->newLine(); } } diff --git a/src/Illuminate/Foundation/Console/OptimizeCommand.php b/src/Illuminate/Foundation/Console/OptimizeCommand.php index 619c2733bc8b..b487928d43ba 100644 --- a/src/Illuminate/Foundation/Console/OptimizeCommand.php +++ b/src/Illuminate/Foundation/Console/OptimizeCommand.php @@ -40,9 +40,13 @@ class OptimizeCommand extends Command */ public function handle() { - $this->call('config:cache'); - $this->call('route:cache'); + $this->components->info('Caching the framework bootstrap files'); - $this->info('Files cached successfully.'); + collect([ + 'config' => fn () => $this->callSilent('config:cache') == 0, + 'routes' => fn () => $this->callSilent('route:cache') == 0, + ])->each(fn ($task, $description) => $this->components->task($description, $task)); + + $this->newLine(); } } diff --git a/src/Illuminate/Foundation/Console/PackageDiscoverCommand.php b/src/Illuminate/Foundation/Console/PackageDiscoverCommand.php index 674c579aa5a2..d9b928f4ad4a 100644 --- a/src/Illuminate/Foundation/Console/PackageDiscoverCommand.php +++ b/src/Illuminate/Foundation/Console/PackageDiscoverCommand.php @@ -42,12 +42,13 @@ class PackageDiscoverCommand extends Command */ public function handle(PackageManifest $manifest) { - $manifest->build(); + $this->components->info('Discovering packages'); - foreach (array_keys($manifest->manifest) as $package) { - $this->line("Discovered Package: {$package}"); - } + $manifest->build(); - $this->info('Package manifest generated successfully.'); + collect($manifest->manifest) + ->keys() + ->each(fn ($description) => $this->components->task($description)) + ->whenNotEmpty(fn () => $this->newLine()); } } diff --git a/src/Illuminate/Foundation/Console/RouteCacheCommand.php b/src/Illuminate/Foundation/Console/RouteCacheCommand.php index 67a1dbde4b8d..00f4050c4572 100644 --- a/src/Illuminate/Foundation/Console/RouteCacheCommand.php +++ b/src/Illuminate/Foundation/Console/RouteCacheCommand.php @@ -63,12 +63,12 @@ public function __construct(Filesystem $files) */ public function handle() { - $this->call('route:clear'); + $this->callSilent('route:clear'); $routes = $this->getFreshApplicationRoutes(); if (count($routes) === 0) { - return $this->error("Your application doesn't have any routes."); + return $this->components->error("Your application doesn't have any routes."); } foreach ($routes as $route) { @@ -79,7 +79,7 @@ public function handle() $this->laravel->getCachedRoutesPath(), $this->buildRouteCacheFile($routes) ); - $this->info('Routes cached successfully.'); + $this->components->info('Routes cached successfully.'); } /** diff --git a/src/Illuminate/Foundation/Console/RouteClearCommand.php b/src/Illuminate/Foundation/Console/RouteClearCommand.php index 0892a6686f6f..da45e6d80f1c 100644 --- a/src/Illuminate/Foundation/Console/RouteClearCommand.php +++ b/src/Illuminate/Foundation/Console/RouteClearCommand.php @@ -63,6 +63,6 @@ public function handle() { $this->files->delete($this->laravel->getCachedRoutesPath()); - $this->info('Route cache cleared successfully.'); + $this->components->info('Route cache cleared successfully.'); } } diff --git a/src/Illuminate/Foundation/Console/RouteListCommand.php b/src/Illuminate/Foundation/Console/RouteListCommand.php index e89cc6645896..caa81652cea9 100644 --- a/src/Illuminate/Foundation/Console/RouteListCommand.php +++ b/src/Illuminate/Foundation/Console/RouteListCommand.php @@ -104,11 +104,11 @@ public function handle() $this->router->flushMiddlewareGroups(); if (! $this->router->getRoutes()->count()) { - return $this->error("Your application doesn't have any routes."); + return $this->components->error("Your application doesn't have any routes."); } if (empty($routes = $this->getRoutes())) { - return $this->error("Your application doesn't have any routes matching the given criteria."); + return $this->components->error("Your application doesn't have any routes matching the given criteria."); } $this->displayRoutes($routes); diff --git a/src/Illuminate/Foundation/Console/StorageLinkCommand.php b/src/Illuminate/Foundation/Console/StorageLinkCommand.php index f5decaf044fa..10427557bff9 100644 --- a/src/Illuminate/Foundation/Console/StorageLinkCommand.php +++ b/src/Illuminate/Foundation/Console/StorageLinkCommand.php @@ -46,7 +46,7 @@ public function handle() foreach ($this->links() as $link => $target) { if (file_exists($link) && ! $this->isRemovableSymlink($link, $this->option('force'))) { - $this->error("The [$link] link already exists."); + $this->components->error("The [$link] link already exists."); continue; } @@ -60,10 +60,8 @@ public function handle() $this->laravel->make('files')->link($target, $link); } - $this->info("The [$link] link has been connected to [$target]."); + $this->components->info("The [$link] link has been connected to [$target]."); } - - $this->info('The links have been created.'); } /** diff --git a/src/Illuminate/Foundation/Console/StubPublishCommand.php b/src/Illuminate/Foundation/Console/StubPublishCommand.php index ada13946162b..9886e0b3f598 100644 --- a/src/Illuminate/Foundation/Console/StubPublishCommand.php +++ b/src/Illuminate/Foundation/Console/StubPublishCommand.php @@ -93,6 +93,6 @@ public function handle() } } - $this->info('Stubs published successfully.'); + $this->components->info('Stubs published successfully.'); } } diff --git a/src/Illuminate/Foundation/Console/UpCommand.php b/src/Illuminate/Foundation/Console/UpCommand.php index 86adaed46154..1562cbfea535 100644 --- a/src/Illuminate/Foundation/Console/UpCommand.php +++ b/src/Illuminate/Foundation/Console/UpCommand.php @@ -44,7 +44,7 @@ public function handle() { try { if (! $this->laravel->maintenanceMode()->active()) { - $this->comment('Application is already up.'); + $this->components->info('Application is already up.'); return 0; } @@ -57,11 +57,12 @@ public function handle() $this->laravel->get('events')->dispatch(MaintenanceModeDisabled::class); - $this->info('Application is now live.'); + $this->components->info('Application is now live.'); } catch (Exception $e) { - $this->error('Failed to disable maintenance mode.'); - - $this->error($e->getMessage()); + $this->components->error(sprintf( + 'Failed to disable maintenance mode: %s.', + $e->getMessage(), + )); return 1; } diff --git a/src/Illuminate/Foundation/Console/VendorPublishCommand.php b/src/Illuminate/Foundation/Console/VendorPublishCommand.php index 69020fba7188..fadbba63e462 100644 --- a/src/Illuminate/Foundation/Console/VendorPublishCommand.php +++ b/src/Illuminate/Foundation/Console/VendorPublishCommand.php @@ -92,8 +92,6 @@ public function handle() foreach ($this->tags ?: [null] as $tag) { $this->publishTag($tag); } - - $this->info('Publishing complete.'); } /** @@ -123,7 +121,7 @@ protected function determineWhatShouldBePublished() */ protected function promptForProviderOrTag() { - $choice = $this->choice( + $choice = $this->components->choice( "Which provider or tag's files would you like to publish?", $choices = $this->publishableChoices() ); @@ -144,8 +142,8 @@ protected function publishableChoices() { return array_merge( ['Publish files from all providers and tags listed below'], - preg_filter('/^/', 'Provider: ', Arr::sort(ServiceProvider::publishableProviders())), - preg_filter('/^/', 'Tag: ', Arr::sort(ServiceProvider::publishableGroups())) + preg_filter('/^/', 'Provider: ', Arr::sort(ServiceProvider::publishableProviders())), + preg_filter('/^/', 'Tag: ', Arr::sort(ServiceProvider::publishableGroups())) ); } @@ -178,16 +176,23 @@ protected function publishTag($tag) $pathsToPublish = $this->pathsToPublish($tag); + if ($publishing = count($pathsToPublish) > 0) { + $this->components->info(sprintf( + 'Publishing %sassets', + $tag ? "[$tag] " : '', + )); + } + foreach ($pathsToPublish as $from => $to) { $this->publishItem($from, $to); - - $published = true; } - if ($published === false) { - $this->comment('No publishable resources for tag ['.$tag.'].'); + if ($publishing === false) { + $this->components->info('No publishable resources for tag ['.$tag.'].'); } else { $this->laravel['events']->dispatch(new VendorTagPublished($tag, $pathsToPublish)); + + $this->newLine(); } } @@ -219,7 +224,7 @@ protected function publishItem($from, $to) return $this->publishDirectory($from, $to); } - $this->error("Can't locate path: <{$from}>"); + $this->components->error("Can't locate path: <{$from}>"); } /** @@ -236,7 +241,12 @@ protected function publishFile($from, $to) $this->files->copy($from, $to); - $this->status($from, $to, 'File'); + $this->status($from, $to, 'file'); + } else { + $this->components->twoColumnDetail(sprintf( + 'File [%s] already exist', + str_replace(base_path().'/', '', realpath($to)), + ), 'SKIPPED'); } } @@ -256,7 +266,7 @@ protected function publishDirectory($from, $to) 'to' => new Flysystem(new LocalAdapter($to, $visibility)), ])); - $this->status($from, $to, 'Directory'); + $this->status($from, $to, 'directory'); } /** @@ -299,10 +309,15 @@ protected function createParentDirectory($directory) */ protected function status($from, $to, $type) { - $from = str_replace(base_path(), '', realpath($from)); + $from = str_replace(base_path().'/', '', realpath($from)); - $to = str_replace(base_path(), '', realpath($to)); + $to = str_replace(base_path().'/', '', realpath($to)); - $this->line('Copied '.$type.' ['.$from.'] To ['.$to.']'); + $this->components->task(sprintf( + 'Copying %s [%s] to [%s]', + $type, + $from, + $to, + )); } } diff --git a/src/Illuminate/Foundation/Console/ViewCacheCommand.php b/src/Illuminate/Foundation/Console/ViewCacheCommand.php index 468d92b9b08b..f3f1282f622e 100644 --- a/src/Illuminate/Foundation/Console/ViewCacheCommand.php +++ b/src/Illuminate/Foundation/Console/ViewCacheCommand.php @@ -43,13 +43,13 @@ class ViewCacheCommand extends Command */ public function handle() { - $this->call('view:clear'); + $this->callSilent('view:clear'); $this->paths()->each(function ($path) { $this->compileViews($this->bladeFilesIn([$path])); }); - $this->info('Blade templates cached successfully.'); + $this->components->info('Blade templates cached successfully.'); } /** diff --git a/src/Illuminate/Foundation/Console/ViewClearCommand.php b/src/Illuminate/Foundation/Console/ViewClearCommand.php index 9aed80bb75b1..8a96fd8abbff 100644 --- a/src/Illuminate/Foundation/Console/ViewClearCommand.php +++ b/src/Illuminate/Foundation/Console/ViewClearCommand.php @@ -74,6 +74,6 @@ public function handle() $this->files->delete($view); } - $this->info('Compiled views cleared successfully.'); + $this->components->info('Compiled views cleared successfully.'); } } diff --git a/src/Illuminate/Foundation/Inspiring.php b/src/Illuminate/Foundation/Inspiring.php index 2cd8e007ccb8..a62da1074ed4 100644 --- a/src/Illuminate/Foundation/Inspiring.php +++ b/src/Illuminate/Foundation/Inspiring.php @@ -94,6 +94,23 @@ public static function quote() 'Nothing in life is to be feared, it is only to be understood. Now is the time to understand more, so that we may fear less. - Marie Curie', 'The biggest battle is the war against ignorance. - Mustafa Kemal Atatürk', 'Always remember that you are absolutely unique. Just like everyone else. - Margaret Mead', - ])->random(); + ])->map(fn ($quote) => static::formatForConsole($quote))->random(); + } + + /** + * Formats the given quote for a pretty console output. + * + * @param string $quote + * @return string + */ + protected static function formatForConsole($quote) + { + [$text, $author] = str($quote)->explode('-'); + + return sprintf( + "\n “ %s ”\n — %s\n", + trim($text), + trim($author), + ); } } diff --git a/src/Illuminate/Notifications/Console/NotificationTableCommand.php b/src/Illuminate/Notifications/Console/NotificationTableCommand.php index 7dd89d438aa5..a0161ce6b7c9 100644 --- a/src/Illuminate/Notifications/Console/NotificationTableCommand.php +++ b/src/Illuminate/Notifications/Console/NotificationTableCommand.php @@ -73,7 +73,7 @@ public function handle() $this->files->put($fullPath, $this->files->get(__DIR__.'/stubs/notifications.stub')); - $this->info('Migration created successfully.'); + $this->components->info('Migration created successfully.'); $this->composer->dumpAutoloads(); } diff --git a/src/Illuminate/Queue/Console/BatchesTableCommand.php b/src/Illuminate/Queue/Console/BatchesTableCommand.php index 8c8708ae3c17..c1ce2d9bb474 100644 --- a/src/Illuminate/Queue/Console/BatchesTableCommand.php +++ b/src/Illuminate/Queue/Console/BatchesTableCommand.php @@ -75,7 +75,7 @@ public function handle() $this->createBaseMigration($table), $table ); - $this->info('Migration created successfully.'); + $this->components->info('Migration created successfully.'); $this->composer->dumpAutoloads(); } diff --git a/src/Illuminate/Queue/Console/ClearCommand.php b/src/Illuminate/Queue/Console/ClearCommand.php index 5729eb8bd4b0..cd3ebb48bd81 100644 --- a/src/Illuminate/Queue/Console/ClearCommand.php +++ b/src/Illuminate/Queue/Console/ClearCommand.php @@ -64,9 +64,9 @@ public function handle() if ($queue instanceof ClearableQueue) { $count = $queue->clear($queueName); - $this->line('Cleared '.$count.' jobs from the ['.$queueName.'] queue '); + $this->components->info('Cleared '.$count.' jobs from the ['.$queueName.'] queue'); } else { - $this->line('Clearing queues is not supported on ['.(new ReflectionClass($queue))->getShortName().'] '); + $this->components->error('Clearing queues is not supported on ['.(new ReflectionClass($queue))->getShortName().']'); } return 0; diff --git a/src/Illuminate/Queue/Console/FailedTableCommand.php b/src/Illuminate/Queue/Console/FailedTableCommand.php index 2996134c6b21..f3f20ccaf19b 100644 --- a/src/Illuminate/Queue/Console/FailedTableCommand.php +++ b/src/Illuminate/Queue/Console/FailedTableCommand.php @@ -75,7 +75,7 @@ public function handle() $this->createBaseMigration($table), $table ); - $this->info('Migration created successfully.'); + $this->components->info('Migration created successfully.'); $this->composer->dumpAutoloads(); } diff --git a/src/Illuminate/Queue/Console/FlushFailedCommand.php b/src/Illuminate/Queue/Console/FlushFailedCommand.php index 085f7734268e..86a5d16c87a5 100644 --- a/src/Illuminate/Queue/Console/FlushFailedCommand.php +++ b/src/Illuminate/Queue/Console/FlushFailedCommand.php @@ -43,11 +43,11 @@ public function handle() $this->laravel['queue.failer']->flush($this->option('hours')); if ($this->option('hours')) { - $this->info("All jobs that failed more than {$this->option('hours')} hours ago have been deleted successfully."); + $this->components->info("All jobs that failed more than {$this->option('hours')} hours ago have been deleted successfully."); return; } - $this->info('All failed jobs deleted successfully.'); + $this->components->info('All failed jobs deleted successfully.'); } } diff --git a/src/Illuminate/Queue/Console/ForgetFailedCommand.php b/src/Illuminate/Queue/Console/ForgetFailedCommand.php index eed4d9ca2e90..f4625e113053 100644 --- a/src/Illuminate/Queue/Console/ForgetFailedCommand.php +++ b/src/Illuminate/Queue/Console/ForgetFailedCommand.php @@ -41,9 +41,9 @@ class ForgetFailedCommand extends Command public function handle() { if ($this->laravel['queue.failer']->forget($this->argument('id'))) { - $this->info('Failed job deleted successfully.'); + $this->components->info('Failed job deleted successfully.'); } else { - $this->error('No failed job matches the given ID.'); + $this->components->error('No failed job matches the given ID.'); } } } diff --git a/src/Illuminate/Queue/Console/ListFailedCommand.php b/src/Illuminate/Queue/Console/ListFailedCommand.php index e74f55afe9be..4ccaf30ea242 100644 --- a/src/Illuminate/Queue/Console/ListFailedCommand.php +++ b/src/Illuminate/Queue/Console/ListFailedCommand.php @@ -49,10 +49,12 @@ class ListFailedCommand extends Command public function handle() { if (count($jobs = $this->getFailedJobs()) === 0) { - return $this->comment('No failed jobs found.'); + return $this->components->info('No failed jobs found.'); } + $this->newLine(); $this->displayFailedJobs($jobs); + $this->newLine(); } /** @@ -122,6 +124,11 @@ protected function matchJobName($payload) */ protected function displayFailedJobs(array $jobs) { - $this->table($this->headers, $jobs); + collect($jobs)->each( + fn ($job) => $this->components->twoColumnDetail( + sprintf('%s %s', $job[4], $job[0]), + sprintf('%s@%s %s', $job[1], $job[2], $job[3]) + ), + ); } } diff --git a/src/Illuminate/Queue/Console/ListenCommand.php b/src/Illuminate/Queue/Console/ListenCommand.php index 57a6f874123f..275905eb017b 100755 --- a/src/Illuminate/Queue/Console/ListenCommand.php +++ b/src/Illuminate/Queue/Console/ListenCommand.php @@ -79,6 +79,8 @@ public function handle() $connection = $this->input->getArgument('connection') ); + $this->components->info(sprintf('Processing jobs from the [%s] %s.', $queue, str('queue')->plural(explode(',', $queue)))); + $this->listener->listen( $connection, $queue, $this->gatherOptions() ); diff --git a/src/Illuminate/Queue/Console/MonitorCommand.php b/src/Illuminate/Queue/Console/MonitorCommand.php index d9d3d007452f..12feda4e11b0 100644 --- a/src/Illuminate/Queue/Console/MonitorCommand.php +++ b/src/Illuminate/Queue/Console/MonitorCommand.php @@ -109,7 +109,7 @@ protected function parseQueues($queues) 'connection' => $connection, 'queue' => $queue, 'size' => $size = $this->manager->connection($connection)->size($queue), - 'status' => $size >= $this->option('max') ? 'ALERT' : 'OK', + 'status' => $size >= $this->option('max') ? 'ALERT' : 'OK', ]; }); } @@ -122,7 +122,17 @@ protected function parseQueues($queues) */ protected function displaySizes(Collection $queues) { - $this->table($this->headers, $queues); + $this->newLine(); + + $this->components->twoColumnDetail('Queue name', 'Size / Status'); + + $queues->each(function ($queue) { + $status = '['.$queue['size'].'] '.$queue['status']; + + $this->components->twoColumnDetail($queue['queue'], $status); + }); + + $this->newLine(); } /** diff --git a/src/Illuminate/Queue/Console/PruneBatchesCommand.php b/src/Illuminate/Queue/Console/PruneBatchesCommand.php index 0feb5040702f..e8a292d34aca 100644 --- a/src/Illuminate/Queue/Console/PruneBatchesCommand.php +++ b/src/Illuminate/Queue/Console/PruneBatchesCommand.php @@ -54,7 +54,7 @@ public function handle() $count = $repository->prune(Carbon::now()->subHours($this->option('hours'))); } - $this->info("{$count} entries deleted!"); + $this->components->info("{$count} entries deleted."); if ($this->option('unfinished')) { $count = 0; @@ -63,7 +63,7 @@ public function handle() $count = $repository->pruneUnfinished(Carbon::now()->subHours($this->option('unfinished'))); } - $this->info("{$count} unfinished entries deleted!"); + $this->components->info("{$count} unfinished entries deleted."); } } } diff --git a/src/Illuminate/Queue/Console/PruneFailedJobsCommand.php b/src/Illuminate/Queue/Console/PruneFailedJobsCommand.php index f6760b193bb7..61f7903b5f11 100644 --- a/src/Illuminate/Queue/Console/PruneFailedJobsCommand.php +++ b/src/Illuminate/Queue/Console/PruneFailedJobsCommand.php @@ -48,11 +48,11 @@ public function handle() if ($failer instanceof PrunableFailedJobProvider) { $count = $failer->prune(Carbon::now()->subHours($this->option('hours'))); } else { - $this->error('The ['.class_basename($failer).'] failed job storage driver does not support pruning.'); + $this->components->error('The ['.class_basename($failer).'] failed job storage driver does not support pruning.'); return 1; } - $this->info("{$count} entries deleted!"); + $this->components->info("{$count} entries deleted."); } } diff --git a/src/Illuminate/Queue/Console/RestartCommand.php b/src/Illuminate/Queue/Console/RestartCommand.php index b104931c82f5..6dfbcb1fa69a 100644 --- a/src/Illuminate/Queue/Console/RestartCommand.php +++ b/src/Illuminate/Queue/Console/RestartCommand.php @@ -66,6 +66,6 @@ public function handle() { $this->cache->forever('illuminate:queue:restart', $this->currentTime()); - $this->info('Broadcasting queue restart signal.'); + $this->components->info('Broadcasting queue restart signal.'); } } diff --git a/src/Illuminate/Queue/Console/RetryBatchCommand.php b/src/Illuminate/Queue/Console/RetryBatchCommand.php index 1ab74fac0ae3..b99e00206866 100644 --- a/src/Illuminate/Queue/Console/RetryBatchCommand.php +++ b/src/Illuminate/Queue/Console/RetryBatchCommand.php @@ -44,17 +44,21 @@ public function handle() $batch = $this->laravel[BatchRepository::class]->find($id = $this->argument('id')); if (! $batch) { - $this->error("Unable to find a batch with ID [{$id}]."); + $this->components->error("Unable to find a batch with ID [{$id}]."); return 1; } elseif (empty($batch->failedJobIds)) { - $this->error('The given batch does not contain any failed jobs.'); + $this->components->error('The given batch does not contain any failed jobs.'); return 1; } + $this->components->info("Pushing failed queue jobs of the batch [$id] back onto the queue."); + foreach ($batch->failedJobIds as $failedJobId) { - $this->call('queue:retry', ['id' => $failedJobId]); + $this->components->task($failedJobId, fn () => $this->callSilent('queue:retry', ['id' => $failedJobId]) == 0); } + + $this->newLine(); } } diff --git a/src/Illuminate/Queue/Console/RetryCommand.php b/src/Illuminate/Queue/Console/RetryCommand.php index c9640f0b0e9e..ef6a5ed49de3 100644 --- a/src/Illuminate/Queue/Console/RetryCommand.php +++ b/src/Illuminate/Queue/Console/RetryCommand.php @@ -48,21 +48,27 @@ class RetryCommand extends Command */ public function handle() { - foreach ($this->getJobIds() as $id) { + $jobsFound = count($ids = $this->getJobIds()) > 0; + + if ($jobsFound) { + $this->components->info('Pushing failed queue jobs back onto the queue.'); + } + + foreach ($ids as $id) { $job = $this->laravel['queue.failer']->find($id); if (is_null($job)) { - $this->error("Unable to find failed job with ID [{$id}]."); + $this->components->error("Unable to find failed job with ID [{$id}]."); } else { $this->laravel['events']->dispatch(new JobRetryRequested($job)); - $this->retryJob($job); - - $this->info("The failed job [{$id}] has been pushed back onto the queue!"); + $this->components->task($id, fn () => $this->retryJob($job)); $this->laravel['queue.failer']->forget($id); } } + + $jobsFound ? $this->newLine() : $this->components->info('No retryable jobs found.'); } /** @@ -103,7 +109,7 @@ protected function getJobIdsByQueue($queue) ->toArray(); if (count($ids) === 0) { - $this->error("Unable to find failed jobs for queue [{$queue}]."); + $this->components->error("Unable to find failed jobs for queue [{$queue}]."); } return $ids; diff --git a/src/Illuminate/Queue/Console/TableCommand.php b/src/Illuminate/Queue/Console/TableCommand.php index 8abb1e5d3a86..aa25dafeb448 100644 --- a/src/Illuminate/Queue/Console/TableCommand.php +++ b/src/Illuminate/Queue/Console/TableCommand.php @@ -75,7 +75,7 @@ public function handle() $this->createBaseMigration($table), $table ); - $this->info('Migration created successfully.'); + $this->components->info('Migration created successfully.'); $this->composer->dumpAutoloads(); } diff --git a/src/Illuminate/Queue/Console/WorkCommand.php b/src/Illuminate/Queue/Console/WorkCommand.php index 66847a5d81e0..2ee07ddcb420 100644 --- a/src/Illuminate/Queue/Console/WorkCommand.php +++ b/src/Illuminate/Queue/Console/WorkCommand.php @@ -12,6 +12,7 @@ use Illuminate\Queue\WorkerOptions; use Illuminate\Support\Carbon; use Symfony\Component\Console\Attribute\AsCommand; +use function Termwind\terminal; #[AsCommand(name: 'queue:work')] class WorkCommand extends Command @@ -71,6 +72,13 @@ class WorkCommand extends Command */ protected $cache; + /** + * Holds the start time of the last processed job, if any. + * + * @var float|null + */ + protected $latestStartedAt; + /** * Create a new queue work command. * @@ -110,6 +118,10 @@ public function handle() // connection being run for the queue operation currently being executed. $queue = $this->getQueue($connection); + $this->components->info( + sprintf('Processing jobs from the [%s] %s.', $queue, str('queue')->plural(explode(',', $queue))) + ); + return $this->runWorker( $connection, $queue ); @@ -184,32 +196,19 @@ protected function listenForEvents() */ protected function writeOutput(Job $job, $status) { - switch ($status) { - case 'starting': - return $this->writeStatus($job, 'Processing', 'comment'); - case 'success': - return $this->writeStatus($job, 'Processed', 'info'); - case 'failed': - return $this->writeStatus($job, 'Failed', 'error'); + if ($status == 'starting') { + $this->latestStartedAt = microtime(true); + $formattedStartedAt = Carbon::now()->format('Y-m-d H:i:s'); + + return $this->output->write(" {$formattedStartedAt} {$job->resolveName()}"); } - } - /** - * Format the status output for the queue worker. - * - * @param \Illuminate\Contracts\Queue\Job $job - * @param string $status - * @param string $type - * @return void - */ - protected function writeStatus(Job $job, $status, $type) - { - $this->output->writeln(sprintf( - "<{$type}>[%s][%s] %s %s", - Carbon::now()->format('Y-m-d H:i:s'), - $job->getJobId(), - str_pad("{$status}:", 11), $job->resolveName() - )); + $runTime = number_format((microtime(true) - $this->latestStartedAt) * 1000, 2).'ms'; + $dots = max(terminal()->width() - mb_strlen($job->resolveName()) - mb_strlen($runTime) - 31, 0); + + $this->output->write(' '.str_repeat('.', $dots)); + $this->output->write(" $runTime"); + $this->output->writeln($status == 'success' ? ' DONE' : ' FAIL'); } /** diff --git a/src/Illuminate/Queue/Listener.php b/src/Illuminate/Queue/Listener.php index 513f75d60c55..d57c209f667b 100755 --- a/src/Illuminate/Queue/Listener.php +++ b/src/Illuminate/Queue/Listener.php @@ -172,7 +172,9 @@ protected function createCommand($connection, $queue, ListenerOptions $options) public function runProcess(Process $process, $memory) { $process->run(function ($type, $line) { - $this->handleWorkerOutput($type, $line); + if (! str($line)->contains('Processing jobs from the')) { + $this->handleWorkerOutput($type, $line); + } }); // Once we have run the job we'll go check if the memory limit has been exceeded diff --git a/src/Illuminate/Routing/Console/ControllerMakeCommand.php b/src/Illuminate/Routing/Console/ControllerMakeCommand.php index 6bec7f4da60e..59dc55fbe5f4 100755 --- a/src/Illuminate/Routing/Console/ControllerMakeCommand.php +++ b/src/Illuminate/Routing/Console/ControllerMakeCommand.php @@ -140,7 +140,7 @@ protected function buildParentReplacements() $parentModelClass = $this->parseModel($this->option('parent')); if (! class_exists($parentModelClass) && - $this->confirm("A {$parentModelClass} model does not exist. Do you want to generate it?", true)) { + $this->components->confirm("A {$parentModelClass} model does not exist. Do you want to generate it?", true)) { $this->call('make:model', ['name' => $parentModelClass]); } @@ -167,7 +167,7 @@ protected function buildModelReplacements(array $replace) { $modelClass = $this->parseModel($this->option('model')); - if (! class_exists($modelClass) && $this->confirm("A {$modelClass} model does not exist. Do you want to generate it?", true)) { + if (! class_exists($modelClass) && $this->components->confirm("A {$modelClass} model does not exist. Do you want to generate it?", true)) { $this->call('make:model', ['name' => $modelClass]); } diff --git a/src/Illuminate/Session/Console/SessionTableCommand.php b/src/Illuminate/Session/Console/SessionTableCommand.php index 04fa8c5fb84a..e44b2da71238 100644 --- a/src/Illuminate/Session/Console/SessionTableCommand.php +++ b/src/Illuminate/Session/Console/SessionTableCommand.php @@ -73,7 +73,7 @@ public function handle() $this->files->put($fullPath, $this->files->get(__DIR__.'/stubs/database.stub')); - $this->info('Migration created successfully.'); + $this->components->info('Migration created successfully.'); $this->composer->dumpAutoloads(); } diff --git a/tests/Console/OutputStyleTest.php b/tests/Console/OutputStyleTest.php new file mode 100644 index 000000000000..ca987bfd7f9c --- /dev/null +++ b/tests/Console/OutputStyleTest.php @@ -0,0 +1,63 @@ +assertFalse($style->newLineWritten()); + + $style->newLine(); + $this->assertTrue($style->newLineWritten()); + } + + public function testDetectsNewLineOnUnderlyingOutput() + { + $bufferedOutput = new BufferedOutput(); + + $underlyingStyle = new OutputStyle(new ArrayInput([]), $bufferedOutput); + $style = new OutputStyle(new ArrayInput([]), $underlyingStyle); + + $underlyingStyle->newLine(); + $this->assertTrue($style->newLineWritten()); + } + + public function testDetectsNewLineOnWrite() + { + $bufferedOutput = new BufferedOutput(); + + $style = new OutputStyle(new ArrayInput([]), $bufferedOutput); + + $style->write('Foo'); + $this->assertFalse($style->newLineWritten()); + + $style->write('Foo', true); + $this->assertTrue($style->newLineWritten()); + } + + public function testDetectsNewLineOnWriteln() + { + $bufferedOutput = new BufferedOutput(); + + $style = new OutputStyle(new ArrayInput([]), $bufferedOutput); + + $style->writeln('Foo'); + $this->assertTrue($style->newLineWritten()); + } +} diff --git a/tests/Console/View/ComponentsTest.php b/tests/Console/View/ComponentsTest.php new file mode 100644 index 000000000000..4bd872f4a6f4 --- /dev/null +++ b/tests/Console/View/ComponentsTest.php @@ -0,0 +1,123 @@ +render('The application is in the [production] environment'); + + $this->assertStringContainsString( + 'THE APPLICATION IS IN THE [PRODUCTION] ENVIRONMENT.', + $output->fetch() + ); + } + + public function testBulletList() + { + $output = new BufferedOutput(); + + with(new Components\BulletList($output))->render([ + 'ls -la', + 'php artisan inspire', + ]); + + $output = $output->fetch(); + + $this->assertStringContainsString('⇂ ls -la', $output); + $this->assertStringContainsString('⇂ php artisan inspire', $output); + } + + public function testError() + { + $output = new BufferedOutput(); + + with(new Components\Error($output))->render('The application is in the [production] environment'); + + $this->assertStringContainsString('ERROR The application is in the [production] environment.', $output->fetch()); + } + + public function testInfo() + { + $output = new BufferedOutput(); + + with(new Components\Info($output))->render('The application is in the [production] environment'); + + $this->assertStringContainsString('INFO The application is in the [production] environment.', $output->fetch()); + } + + public function testConfirm() + { + $output = m::mock(OutputStyle::class); + + $output->shouldReceive('confirm') + ->with('Question?', true) + ->once() + ->andReturnTrue(); + + $result = with(new Components\Confirm($output))->render('Question?'); + $this->assertTrue($result); + } + + public function testChoice() + { + $output = m::mock(OutputStyle::class); + + $output->shouldReceive('askQuestion') + ->with(m::type(ChoiceQuestion::class)) + ->once() + ->andReturn('a'); + + $result = with(new Components\Choice($output))->render('Question?', ['a', 'b']); + $this->assertSame('a', $result); + } + + public function testTask() + { + $output = new BufferedOutput(); + + with(new Components\Task($output))->render('My task', fn () => true); + $result = $output->fetch(); + $this->assertStringContainsString('My task', $result); + $this->assertStringContainsString('DONE', $result); + + with(new Components\Task($output))->render('My task', fn () => false); + $result = $output->fetch(); + $this->assertStringContainsString('My task', $result); + $this->assertStringContainsString('FAIL', $result); + } + + public function testTwoColumnDetail() + { + $output = new BufferedOutput(); + + with(new Components\TwoColumnDetail($output))->render('First', 'Second'); + $result = $output->fetch(); + $this->assertStringContainsString('First', $result); + $this->assertStringContainsString('Second', $result); + } + + public function testWarn() + { + $output = new BufferedOutput(); + + with(new Components\Warn($output))->render('The application is in the [production] environment'); + + $this->assertStringContainsString('WARN The application is in the [production] environment.', $output->fetch()); + } +} diff --git a/tests/Database/DatabaseMigrationMigrateCommandTest.php b/tests/Database/DatabaseMigrationMigrateCommandTest.php index 2fdffd062bbf..47afe368f291 100755 --- a/tests/Database/DatabaseMigrationMigrateCommandTest.php +++ b/tests/Database/DatabaseMigrationMigrateCommandTest.php @@ -68,7 +68,7 @@ public function testMigrationsCanBeRunWithStoredSchema() public function testMigrationRepositoryCreatedWhenNecessary() { $params = [$migrator = m::mock(Migrator::class), $dispatcher = m::mock(Dispatcher::class)]; - $command = $this->getMockBuilder(MigrateCommand::class)->onlyMethods(['call'])->setConstructorArgs($params)->getMock(); + $command = $this->getMockBuilder(MigrateCommand::class)->onlyMethods(['callSilent'])->setConstructorArgs($params)->getMock(); $app = new ApplicationDatabaseMigrationStub(['path.database' => __DIR__]); $app->useDatabasePath(__DIR__); $command->setLaravel($app); @@ -80,7 +80,7 @@ public function testMigrationRepositoryCreatedWhenNecessary() $migrator->shouldReceive('setOutput')->once()->andReturn($migrator); $migrator->shouldReceive('run')->once()->with([__DIR__.DIRECTORY_SEPARATOR.'migrations'], ['pretend' => false, 'step' => false]); $migrator->shouldReceive('repositoryExists')->once()->andReturn(false); - $command->expects($this->once())->method('call')->with($this->equalTo('migrate:install'), $this->equalTo([])); + $command->expects($this->once())->method('callSilent')->with($this->equalTo('migrate:install'), $this->equalTo([])); $this->runCommand($command); } diff --git a/tests/Database/DatabaseMigratorIntegrationTest.php b/tests/Database/DatabaseMigratorIntegrationTest.php index f144170da0a1..7870cdb3e9df 100644 --- a/tests/Database/DatabaseMigratorIntegrationTest.php +++ b/tests/Database/DatabaseMigratorIntegrationTest.php @@ -59,7 +59,9 @@ protected function setUp(): void ); $output = m::mock(OutputStyle::class); + $output->shouldReceive('write'); $output->shouldReceive('writeln'); + $output->shouldReceive('newLineWritten'); $this->migrator->setOutput($output); diff --git a/tests/Database/DatabaseSeederTest.php b/tests/Database/DatabaseSeederTest.php index 7926a1ff2a27..907f47d7b4ff 100755 --- a/tests/Database/DatabaseSeederTest.php +++ b/tests/Database/DatabaseSeederTest.php @@ -46,8 +46,7 @@ public function testCallResolveTheClassAndCallsRun() $child->shouldReceive('setContainer')->once()->with($container)->andReturn($child); $child->shouldReceive('setCommand')->once()->with($command)->andReturn($child); $child->shouldReceive('__invoke')->once(); - $command->shouldReceive('getOutput')->once()->andReturn($output); - $output->shouldReceive('writeln')->once(); + $output->shouldReceive('write')->times(3); $seeder->call('ClassName'); } diff --git a/tests/Database/PruneCommandTest.php b/tests/Database/PruneCommandTest.php index 58926b43e42b..cbb38ff1930b 100644 --- a/tests/Database/PruneCommandTest.php +++ b/tests/Database/PruneCommandTest.php @@ -35,21 +35,37 @@ public function testPrunableModelWithPrunableRecords() { $output = $this->artisan(['--model' => PrunableTestModelWithPrunableRecords::class]); - $this->assertEquals(<<<'EOF' -10 [Illuminate\Tests\Database\PrunableTestModelWithPrunableRecords] records have been pruned. -20 [Illuminate\Tests\Database\PrunableTestModelWithPrunableRecords] records have been pruned. - -EOF, str_replace("\r", '', $output->fetch())); + $output = $output->fetch(); + + $this->assertStringContainsString( + 'Illuminate\Tests\Database\PrunableTestModelWithPrunableRecords', + $output, + ); + + $this->assertStringContainsString( + '10 records', + $output, + ); + + $this->assertStringContainsString( + 'Illuminate\Tests\Database\PrunableTestModelWithPrunableRecords', + $output, + ); + + $this->assertStringContainsString( + '20 records', + $output, + ); } public function testPrunableTestModelWithoutPrunableRecords() { $output = $this->artisan(['--model' => PrunableTestModelWithoutPrunableRecords::class]); - $this->assertEquals(<<<'EOF' -No prunable [Illuminate\Tests\Database\PrunableTestModelWithoutPrunableRecords] records found. - -EOF, str_replace("\r", '', $output->fetch())); + $this->assertStringContainsString( + 'No prunable [Illuminate\Tests\Database\PrunableTestModelWithoutPrunableRecords] records found.', + $output->fetch() + ); } public function testPrunableSoftDeletedModelWithPrunableRecords() @@ -74,10 +90,17 @@ public function testPrunableSoftDeletedModelWithPrunableRecords() $output = $this->artisan(['--model' => PrunableTestSoftDeletedModelWithPrunableRecords::class]); - $this->assertEquals(<<<'EOF' -2 [Illuminate\Tests\Database\PrunableTestSoftDeletedModelWithPrunableRecords] records have been pruned. + $output = $output->fetch(); -EOF, str_replace("\r", '', $output->fetch())); + $this->assertStringContainsString( + 'Illuminate\Tests\Database\PrunableTestSoftDeletedModelWithPrunableRecords', + $output, + ); + + $this->assertStringContainsString( + '2 records', + $output, + ); $this->assertEquals(2, PrunableTestSoftDeletedModelWithPrunableRecords::withTrashed()->count()); } @@ -86,20 +109,20 @@ public function testNonPrunableTest() { $output = $this->artisan(['--model' => NonPrunableTestModel::class]); - $this->assertEquals(<<<'EOF' -No prunable [Illuminate\Tests\Database\NonPrunableTestModel] records found. - -EOF, str_replace("\r", '', $output->fetch())); + $this->assertStringContainsString( + 'No prunable [Illuminate\Tests\Database\NonPrunableTestModel] records found.', + $output->fetch(), + ); } public function testNonPrunableTestWithATrait() { $output = $this->artisan(['--model' => NonPrunableTrait::class]); - $this->assertEquals(<<<'EOF' -No prunable models found. - -EOF, str_replace("\r", '', $output->fetch())); + $this->assertStringContainsString( + 'No prunable models found.', + $output->fetch(), + ); } public function testTheCommandMayBePretended() @@ -128,10 +151,10 @@ public function testTheCommandMayBePretended() '--pretend' => true, ]); - $this->assertEquals(<<<'EOF' -3 [Illuminate\Tests\Database\PrunableTestModelWithPrunableRecords] records will be pruned. - -EOF, str_replace("\r", '', $output->fetch())); + $this->assertStringContainsString( + '3 [Illuminate\Tests\Database\PrunableTestModelWithPrunableRecords] records will be pruned.', + $output->fetch(), + ); $this->assertEquals(5, PrunableTestModelWithPrunableRecords::count()); } @@ -161,10 +184,10 @@ public function testTheCommandMayBePretendedOnSoftDeletedModel() '--pretend' => true, ]); - $this->assertEquals(<<<'EOF' -2 [Illuminate\Tests\Database\PrunableTestSoftDeletedModelWithPrunableRecords] records will be pruned. - -EOF, str_replace("\r", '', $output->fetch())); + $this->assertStringContainsString( + '2 [Illuminate\Tests\Database\PrunableTestSoftDeletedModelWithPrunableRecords] records will be pruned.', + $output->fetch(), + ); $this->assertEquals(4, PrunableTestSoftDeletedModelWithPrunableRecords::withTrashed()->count()); } diff --git a/tests/Integration/Console/Events/EventListCommandTest.php b/tests/Integration/Console/Events/EventListCommandTest.php index 8f8cd1e17f4c..3273c7da33c1 100644 --- a/tests/Integration/Console/Events/EventListCommandTest.php +++ b/tests/Integration/Console/Events/EventListCommandTest.php @@ -22,7 +22,7 @@ public function testDisplayEmptyList() { $this->artisan(EventListCommand::class) ->assertSuccessful() - ->expectsOutput("Your application doesn't have any events matching the given criteria."); + ->expectsOutputToContain("Your application doesn't have any events matching the given criteria."); } public function testDisplayEvents() @@ -37,15 +37,12 @@ public function testDisplayEvents() $this->artisan(EventListCommand::class) ->assertSuccessful() - ->expectsOutput(' ExampleSubscriberEventName') - ->expectsOutput(' ⇂ Illuminate\Tests\Integration\Console\Events\ExampleSubscriber@a') - ->expectsOutput(' ⇂ Illuminate\Tests\Integration\Console\Events\ExampleSubscriber@b') - ->expectsOutput(' Illuminate\Tests\Integration\Console\Events\ExampleBroadcastEvent (ShouldBroadcast)') - ->expectsOutput(' ⇂ Illuminate\Tests\Integration\Console\Events\ExampleBroadcastListener') - ->expectsOutput(' Illuminate\Tests\Integration\Console\Events\ExampleEvent') - ->expectsOutput(' ⇂ Illuminate\Tests\Integration\Console\Events\ExampleListener') - ->expectsOutput(' ⇂ Illuminate\Tests\Integration\Console\Events\ExampleQueueListener (ShouldQueue)') - ->expectsOutput(' ⇂ Closure at: '.$unixFilePath.':'.$closureLineNumber); + ->expectsOutputToContain('ExampleSubscriberEventName') + ->expectsOutputToContain('⇂ Illuminate\Tests\Integration\Console\Events\ExampleSubscriber@a') + ->expectsOutputToContain('Illuminate\Tests\Integration\Console\Events\ExampleBroadcastEvent (ShouldBroadcast)') + ->expectsOutputToContain('⇂ Illuminate\Tests\Integration\Console\Events\ExampleBroadcastListener') + ->expectsOutputToContain('Illuminate\Tests\Integration\Console\Events\ExampleEvent') + ->expectsOutputToContain('⇂ Closure at: '.$unixFilePath.':'.$closureLineNumber); } public function testDisplayFilteredEvent() diff --git a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php index 891be7452413..73b8c247e242 100644 --- a/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleListCommandTest.php @@ -25,7 +25,7 @@ public function testDisplayEmptySchedule() { $this->artisan(ScheduleListCommand::class) ->assertSuccessful() - ->expectsOutput('No scheduled tasks have been defined.'); + ->expectsOutputToContain('No scheduled tasks have been defined.'); } public function testDisplaySchedule() diff --git a/tests/Integration/Console/Scheduling/ScheduleTestCommandTest.php b/tests/Integration/Console/Scheduling/ScheduleTestCommandTest.php index 74b9170e2014..66e81e74883d 100644 --- a/tests/Integration/Console/Scheduling/ScheduleTestCommandTest.php +++ b/tests/Integration/Console/Scheduling/ScheduleTestCommandTest.php @@ -17,7 +17,6 @@ protected function setUp(): void Carbon::setTestNow(now()->startOfYear()); - $this->timestamp = now()->startOfYear()->format('c'); $this->schedule = $this->app->make(Schedule::class); } @@ -25,7 +24,7 @@ public function testRunNoDefinedCommands() { $this->artisan(ScheduleTestCommand::class) ->assertSuccessful() - ->expectsOutput('No scheduled commands have been defined.'); + ->expectsOutputToContain('No scheduled commands have been defined.'); } public function testRunNoMatchingCommand() @@ -34,7 +33,7 @@ public function testRunNoMatchingCommand() $this->artisan(ScheduleTestCommand::class, ['--name' => 'missing:command']) ->assertSuccessful() - ->expectsOutput('No matching scheduled command found.'); + ->expectsOutputToContain('No matching scheduled command found.'); } public function testRunUsingNameOption() @@ -43,17 +42,21 @@ public function testRunUsingNameOption() $this->schedule->job(BarJobStub::class); $this->schedule->call(fn () => true)->name('callback'); + $expectedOutput = windows_os() + ? 'Running ["artisan" bar:command]' + : "Running ['artisan' bar:command]"; + $this->artisan(ScheduleTestCommand::class, ['--name' => 'bar:command']) ->assertSuccessful() - ->expectsOutput(sprintf('[%s] Running scheduled command: bar-command', $this->timestamp)); + ->expectsOutputToContain($expectedOutput); $this->artisan(ScheduleTestCommand::class, ['--name' => BarJobStub::class]) ->assertSuccessful() - ->expectsOutput(sprintf('[%s] Running scheduled command: %s', $this->timestamp, BarJobStub::class)); + ->expectsOutputToContain(sprintf('Running [%s]', BarJobStub::class)); $this->artisan(ScheduleTestCommand::class, ['--name' => 'callback']) ->assertSuccessful() - ->expectsOutput(sprintf('[%s] Running scheduled command: callback', $this->timestamp)); + ->expectsOutputToContain('Running [callback]'); } public function testRunUsingChoices() @@ -70,7 +73,7 @@ public function testRunUsingChoices() [Application::formatCommandString('bar:command'), BarJobStub::class, 'callback'], true ) - ->expectsOutput(sprintf('[%s] Running scheduled command: callback', $this->timestamp)); + ->expectsOutputToContain('Running [callback]'); } protected function tearDown(): void diff --git a/tests/Integration/Migration/MigratorTest.php b/tests/Integration/Migration/MigratorTest.php index 67afc1748eb5..2168c289ebbf 100644 --- a/tests/Integration/Migration/MigratorTest.php +++ b/tests/Integration/Migration/MigratorTest.php @@ -26,12 +26,11 @@ protected function setUp(): void public function testMigrate() { - $this->expectOutput('Migrating: 2014_10_12_000000_create_people_table'); - $this->expectOutput(m::pattern('#Migrated: 2014_10_12_000000_create_people_table (.*)#')); - $this->expectOutput('Migrating: 2015_10_04_000000_modify_people_table'); - $this->expectOutput(m::pattern('#Migrated: 2015_10_04_000000_modify_people_table (.*)#')); - $this->expectOutput('Migrating: 2016_10_04_000000_modify_people_table'); - $this->expectOutput(m::pattern('#Migrated: 2016_10_04_000000_modify_people_table (.*)#')); + $this->expectInfo('Running migrations.'); + + $this->expectTask('2014_10_12_000000_create_people_table', 'DONE'); + $this->expectTask('2015_10_04_000000_modify_people_table', 'DONE'); + $this->expectTask('2016_10_04_000000_modify_people_table', 'DONE'); $this->subject->run([__DIR__.'/fixtures']); @@ -47,12 +46,11 @@ public function testRollback() $this->subject->getRepository()->log('2015_10_04_000000_modify_people_table', 1); $this->subject->getRepository()->log('2016_10_04_000000_modify_people_table', 1); - $this->expectOutput('Rolling back: 2016_10_04_000000_modify_people_table'); - $this->expectOutput(m::pattern('#Rolled back: 2016_10_04_000000_modify_people_table (.*)#')); - $this->expectOutput('Rolling back: 2015_10_04_000000_modify_people_table'); - $this->expectOutput(m::pattern('#Rolled back: 2015_10_04_000000_modify_people_table (.*)#')); - $this->expectOutput('Rolling back: 2014_10_12_000000_create_people_table'); - $this->expectOutput(m::pattern('#Rolled back: 2014_10_12_000000_create_people_table (.*)#')); + $this->expectInfo('Rollbacking migrations.'); + + $this->expectTask('2016_10_04_000000_modify_people_table', 'DONE'); + $this->expectTask('2015_10_04_000000_modify_people_table', 'DONE'); + $this->expectTask('2014_10_12_000000_create_people_table', 'DONE'); $this->subject->rollback([__DIR__.'/fixtures']); @@ -61,18 +59,76 @@ public function testRollback() public function testPretendMigrate() { - $this->expectOutput('CreatePeopleTable: create table "people" ("id" integer not null primary key autoincrement, "name" varchar not null, "email" varchar not null, "password" varchar not null, "remember_token" varchar, "created_at" datetime, "updated_at" datetime)'); - $this->expectOutput('CreatePeopleTable: create unique index "people_email_unique" on "people" ("email")'); - $this->expectOutput('ModifyPeopleTable: alter table "people" add column "first_name" varchar'); - $this->expectOutput('2016_10_04_000000_modify_people_table: alter table "people" add column "last_name" varchar'); + $this->expectInfo('Running migrations.'); + + $this->expectTwoColumnDetail('CreatePeopleTable'); + $this->expectBulletList([ + 'create table "people" ("id" integer not null primary key autoincrement, "name" varchar not null, "email" varchar not null, "password" varchar not null, "remember_token" varchar, "created_at" datetime, "updated_at" datetime)', + 'create unique index "people_email_unique" on "people" ("email")', + ]); + + $this->expectTwoColumnDetail('ModifyPeopleTable'); + $this->expectBulletList(['alter table "people" add column "first_name" varchar']); + + $this->expectTwoColumnDetail('2016_10_04_000000_modify_people_table'); + $this->expectBulletList(['alter table "people" add column "last_name" varchar']); $this->subject->run([__DIR__.'/fixtures'], ['pretend' => true]); $this->assertFalse(DB::getSchemaBuilder()->hasTable('people')); } - private function expectOutput($argument): void + protected function expectInfo($message): void + { + $this->output->shouldReceive('writeln')->once()->with(m::on( + fn ($argument) => str($argument)->contains($message), + ), m::any()); + } + + protected function expectTwoColumnDetail($first, $second = null) + { + $this->output->shouldReceive('writeln')->with(m::on(function ($argument) use ($first, $second) { + $result = str($argument)->contains($first); + + if ($result && $second) { + $result = str($argument)->contains($second); + } + + return $result; + }), m::any()); + } + + protected function expectBulletList($elements): void + { + $this->output->shouldReceive('writeln')->once()->with(m::on(function ($argument) use ($elements) { + foreach ($elements as $element) { + if (! str($argument)->contains("⇂ $element")) { + return false; + } + } + + return true; + }), m::any()); + } + + protected function expectTask($description, $result): void { - $this->output->shouldReceive('writeln')->once()->with($argument); + // Ignore dots... + $this->output->shouldReceive('write')->with(m::on( + fn ($argument) => str($argument)->contains(['', '.']), + ), m::any(), m::any()); + + // Ignore duration... + $this->output->shouldReceive('write')->with(m::on( + fn ($argument) => str($argument)->contains(['ms']), + ), m::any(), m::any()); + + $this->output->shouldReceive('write')->once()->with(m::on( + fn ($argument) => str($argument)->contains($description), + ), m::any(), m::any()); + + $this->output->shouldReceive('writeln')->once()->with(m::on( + fn ($argument) => str($argument)->contains($result), + ), m::any()); } }