From 21d63d5e01e3d36ead963313581e7db14d3d1473 Mon Sep 17 00:00:00 2001 From: Deeka Wong Date: Mon, 26 Jun 2023 16:37:32 +0800 Subject: [PATCH] Improve Command (#5878) --- src/command/src/Command.php | 300 +------------ src/command/src/Concerns/HasParameters.php | 63 +++ src/command/src/Concerns/InteractsWithIO.php | 425 +++++++++++++++++++ 3 files changed, 510 insertions(+), 278 deletions(-) create mode 100644 src/command/src/Concerns/HasParameters.php create mode 100644 src/command/src/Concerns/InteractsWithIO.php diff --git a/src/command/src/Command.php b/src/command/src/Command.php index 76ba0ba5c..9b989c07a 100644 --- a/src/command/src/Command.php +++ b/src/command/src/Command.php @@ -11,20 +11,13 @@ declare(strict_types=1); */ namespace Hyperf\Command; -use Hyperf\Contract\Arrayable; use Hyperf\Coroutine\Coroutine; -use Hyperf\Stringable\Str; use Psr\EventDispatcher\EventDispatcherInterface; use Swoole\ExitException; use Symfony\Component\Console\Command\Command as SymfonyCommand; -use Symfony\Component\Console\Formatter\OutputFormatterStyle; -use Symfony\Component\Console\Helper\Table; -use Symfony\Component\Console\Helper\TableStyle; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\ChoiceQuestion; -use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; use Throwable; @@ -36,33 +29,32 @@ use function Hyperf\Tappable\tap; abstract class Command extends SymfonyCommand { use DisableEventDispatcher; + use Concerns\InteractsWithIO; + use Concerns\HasParameters; /** * The name of the command. */ protected ?string $name = null; + /** + * The description of the command. + */ protected string $description = ''; - protected ?InputInterface $input = null; - - /** - * @var null|SymfonyStyle - */ - protected ?OutputInterface $output = null; - - /** - * The default verbosity of output commands. - */ - protected int $verbosity = OutputInterface::VERBOSITY_NORMAL; - /** * Execution in a coroutine environment. */ protected bool $coroutine = true; + /** + * The eventDispatcher. + */ protected ?EventDispatcherInterface $eventDispatcher = null; + /** + * The hookFlags of the command. + */ protected int $hookFlags = -1; /** @@ -70,18 +62,6 @@ abstract class Command extends SymfonyCommand */ protected ?string $signature = null; - /** - * The mapping between human-readable verbosity levels and Symfony's OutputInterface. - */ - protected array $verbosityMap - = [ - 'v' => OutputInterface::VERBOSITY_VERBOSE, - 'vv' => OutputInterface::VERBOSITY_VERY_VERBOSE, - 'vvv' => OutputInterface::VERBOSITY_DEBUG, - 'quiet' => OutputInterface::VERBOSITY_QUIET, - 'normal' => OutputInterface::VERBOSITY_NORMAL, - ]; - /** * The exit code of the command. */ @@ -101,9 +81,15 @@ abstract class Command extends SymfonyCommand parent::__construct($this->name); } - ! empty($this->description) && $this->setDescription($this->description); - $this->addDisableDispatcherOption(); + + if (! empty($this->description)) { + $this->setDescription($this->description); + } + + if (! isset($this->signature)) { + $this->specifyParameters(); + } } /** @@ -116,195 +102,6 @@ abstract class Command extends SymfonyCommand return parent::run($this->input = $input, $this->output); } - /** - * Confirm a question with the user. - */ - public function confirm(string $question, bool $default = false): bool - { - return $this->output->confirm($question, $default); - } - - /** - * Prompt the user for input. - */ - public function ask(string $question, string $default = null) - { - return $this->output->ask($question, $default); - } - - /** - * Prompt the user for input with auto-completion. - * - * @param null|bool|float|int|string $default - */ - public function anticipate(string $question, array $choices, $default = null) - { - return $this->askWithCompletion($question, $choices, $default); - } - - /** - * Prompt the user for input with auto-completion. - * - * @param null|bool|float|int|string $default - */ - public function askWithCompletion(string $question, array $choices, $default = null) - { - $question = new Question($question, $default); - - $question->setAutocompleterValues($choices); - - return $this->output->askQuestion($question); - } - - /** - * Prompt the user for input but hide the answer from the console. - */ - public function secret(string $question, bool $fallback = true) - { - $question = new Question($question); - - $question->setHidden(true)->setHiddenFallback($fallback); - - return $this->output->askQuestion($question); - } - - /** - * Give the user a multiple choice from an array of answers. - * @param mixed $default - */ - public function choiceMultiple( - string $question, - array $choices, - $default = null, - ?int $attempts = null - ): array { - $question = new ChoiceQuestion($question, $choices, $default); - - $question->setMaxAttempts($attempts)->setMultiselect(true); - - return $this->output->askQuestion($question); - } - - /** - * Give the user a single choice from an array of answers. - * - * @param mixed $default - */ - public function choice( - string $question, - array $choices, - $default = null, - ?int $attempts = null - ): mixed { - return $this->choiceMultiple($question, $choices, $default, $attempts)[0]; - } - - /** - * Format input to textual table. - */ - public function table(array $headers, array|Arrayable $rows, TableStyle|string $tableStyle = 'default', array $columnStyles = []): void - { - $table = new Table($this->output); - - if ($rows instanceof Arrayable) { - $rows = $rows->toArray(); - } - - $table->setHeaders($headers)->setRows($rows)->setStyle($tableStyle); - - foreach ($columnStyles as $columnIndex => $columnStyle) { - $table->setColumnStyle($columnIndex, $columnStyle); - } - - $table->render(); - } - - /** - * Write a string as standard output. - * - * @param mixed $string - * @param null|mixed $style - * @param null|mixed $verbosity - */ - public function line($string, $style = null, $verbosity = null) - { - $styled = $style ? "<{$style}>{$string}" : $string; - $this->output->writeln($styled, $this->parseVerbosity($verbosity)); - } - - /** - * Write a string as information output. - * - * @param mixed $string - * @param null|mixed $verbosity - */ - public function info($string, $verbosity = null) - { - $this->line($string, 'info', $verbosity); - } - - /** - * Write a string as comment output. - * - * @param mixed $string - * @param null|mixed $verbosity - */ - public function comment($string, $verbosity = null) - { - $this->line($string, 'comment', $verbosity); - } - - /** - * Write a string as question output. - * - * @param mixed $string - * @param null|mixed $verbosity - */ - public function question($string, $verbosity = null) - { - $this->line($string, 'question', $verbosity); - } - - /** - * Write a string as error output. - * - * @param mixed $string - * @param null|mixed $verbosity - */ - public function error($string, $verbosity = null) - { - $this->line($string, 'error', $verbosity); - } - - /** - * Write a string as warning output. - * - * @param mixed $string - * @param null|mixed $verbosity - */ - public function warn($string, $verbosity = null) - { - if (! $this->output->getFormatter()->hasStyle('warning')) { - $style = new OutputFormatterStyle('yellow'); - $this->output->getFormatter()->setStyle('warning', $style); - } - $this->line($string, 'warning', $verbosity); - } - - /** - * Write a string in an alert box. - * - * @param mixed $string - */ - public function alert($string) - { - $length = Str::length(strip_tags($string)) + 12; - $this->comment(str_repeat('*', $length)); - $this->comment('* ' . $string . ' *'); - $this->comment(str_repeat('*', $length)); - $this->output->newLine(); - } - /** * Call another console command. */ @@ -315,36 +112,6 @@ abstract class Command extends SymfonyCommand return $this->getApplication()->find($command)->run($this->createInputFromArguments($arguments), $this->output); } - /** - * Handle the current command. - */ - abstract public function handle(); - - /** - * Set the verbosity level. - * - * @param mixed $level - */ - protected function setVerbosity($level) - { - $this->verbosity = $this->parseVerbosity($level); - } - - /** - * Get the verbosity level in terms of Symfony's OutputInterface level. - * - * @param null|mixed $level - */ - protected function parseVerbosity($level = null): int - { - if (isset($this->verbosityMap[$level])) { - $level = $this->verbosityMap[$level]; - } elseif (! is_int($level)) { - $level = $this->verbosity; - } - return $level; - } - /** * Create an input instance from the given arguments. */ @@ -373,27 +140,6 @@ abstract class Command extends SymfonyCommand })->all(); } - /** - * Specify the arguments and options on the command. - */ - protected function specifyParameters(): void - { - // We will loop through all the arguments and options for the command and - // set them all on the base command instance. This specifies what can get - // past into these commands as "parameters" to control the execution. - if (method_exists($this, 'getArguments')) { - foreach ($this->getArguments() ?? [] as $arguments) { - call_user_func_array([$this, 'addArgument'], $arguments); - } - } - - if (method_exists($this, 'getOptions')) { - foreach ($this->getOptions() ?? [] as $options) { - call_user_func_array([$this, 'addOption'], $options); - } - } - } - /** * Configure the console command using a fluent definition. */ @@ -413,19 +159,17 @@ abstract class Command extends SymfonyCommand protected function configure() { parent::configure(); - if (! isset($this->signature)) { - $this->specifyParameters(); - } } protected function execute(InputInterface $input, OutputInterface $output) { $this->disableDispatcher($input); + $method = method_exists($this, 'handle') ? 'handle' : '__invoke'; - $callback = function () { + $callback = function () use ($method) { try { $this->eventDispatcher?->dispatch(new Event\BeforeHandle($this)); - $this->handle(); + $this->{$method}(); $this->eventDispatcher?->dispatch(new Event\AfterHandle($this)); } catch (Throwable $exception) { if (class_exists(ExitException::class) && $exception instanceof ExitException) { diff --git a/src/command/src/Concerns/HasParameters.php b/src/command/src/Concerns/HasParameters.php new file mode 100644 index 000000000..bba0f9258 --- /dev/null +++ b/src/command/src/Concerns/HasParameters.php @@ -0,0 +1,63 @@ +getArguments() as $arguments) { + if ($arguments instanceof InputArgument) { + $this->getDefinition()->addArgument($arguments); + } else { + $this->addArgument(...$arguments); + } + } + + foreach ($this->getOptions() as $options) { + if ($options instanceof InputOption) { + $this->getDefinition()->addOption($options); + } else { + $this->addOption(...$options); + } + } + } + + /** + * Get the console command arguments. + * + * @return array + */ + protected function getArguments() + { + return []; + } + + /** + * Get the console command options. + * + * @return array + */ + protected function getOptions() + { + return []; + } +} diff --git a/src/command/src/Concerns/InteractsWithIO.php b/src/command/src/Concerns/InteractsWithIO.php new file mode 100644 index 000000000..85d7ef612 --- /dev/null +++ b/src/command/src/Concerns/InteractsWithIO.php @@ -0,0 +1,425 @@ + OutputInterface::VERBOSITY_VERBOSE, + 'vv' => OutputInterface::VERBOSITY_VERY_VERBOSE, + 'vvv' => OutputInterface::VERBOSITY_DEBUG, + 'quiet' => OutputInterface::VERBOSITY_QUIET, + 'normal' => OutputInterface::VERBOSITY_NORMAL, + ]; + + /** + * Determine if the given argument is present. + * + * @param int|string $name + * @return bool + */ + public function hasArgument($name) + { + return $this->input->hasArgument($name); + } + + /** + * Get the value of a command argument. + * + * @param null|string $key + * @return null|array|bool|string + */ + public function argument($key = null) + { + if (is_null($key)) { + return $this->input->getArguments(); + } + + return $this->input->getArgument($key); + } + + /** + * Get all of the arguments passed to the command. + * + * @return array + */ + public function arguments() + { + return $this->argument(); + } + + /** + * Determine if the given option is present. + * + * @param string $name + * @return bool + */ + public function hasOption($name) + { + return $this->input->hasOption($name); + } + + /** + * Get the value of a command option. + * + * @param null|string $key + * @return null|array|bool|string + */ + public function option($key = null) + { + if (is_null($key)) { + return $this->input->getOptions(); + } + + return $this->input->getOption($key); + } + + /** + * Get all of the options passed to the command. + * + * @return array + */ + public function options() + { + return $this->option(); + } + + /** + * Confirm a question with the user. + * + * @param string $question + * @param bool $default + * @return bool + */ + public function confirm($question, $default = false) + { + return $this->output->confirm($question, $default); + } + + /** + * Prompt the user for input. + * + * @param string $question + * @param null|string $default + * @return mixed + */ + public function ask($question, $default = null) + { + return $this->output->ask($question, $default); + } + + /** + * Prompt the user for input with auto completion. + * + * @param string $question + * @param array|callable $choices + * @param null|string $default + * @return mixed + */ + public function anticipate($question, $choices, $default = null) + { + return $this->askWithCompletion($question, $choices, $default); + } + + /** + * Prompt the user for input with auto completion. + * + * @param string $question + * @param array|callable $choices + * @param null|string $default + * @return mixed + */ + public function askWithCompletion($question, $choices, $default = null) + { + $question = new Question($question, $default); + + is_callable($choices) + ? $question->setAutocompleterCallback($choices) + : $question->setAutocompleterValues($choices); + + return $this->output->askQuestion($question); + } + + /** + * Prompt the user for input but hide the answer from the console. + * + * @param string $question + * @param bool $fallback + * @return mixed + */ + public function secret($question, $fallback = true) + { + $question = new Question($question); + + $question->setHidden(true)->setHiddenFallback($fallback); + + return $this->output->askQuestion($question); + } + + /** + * Give the user a single choice from an array of answers. + * + * @param string $question + * @param null|int|string $default + * @param null|mixed $attempts + * @param bool $multiple + * @return array|string + */ + public function choice($question, array $choices, $default = null, $attempts = null, $multiple = false) + { + $question = new ChoiceQuestion($question, $choices, $default); + + $question->setMaxAttempts($attempts)->setMultiselect($multiple); + + return $this->output->askQuestion($question); + } + + /** + * Format input to textual table. + * + * @param array $headers + * @param array|Arrayable $rows + * @param string|\Symfony\Component\Console\Helper\TableStyle $tableStyle + */ + public function table($headers, $rows, $tableStyle = 'default', array $columnStyles = []) + { + $table = new Table($this->output); + + if ($rows instanceof Arrayable) { + $rows = $rows->toArray(); + } + + $table->setHeaders((array) $headers)->setRows($rows)->setStyle($tableStyle); + + foreach ($columnStyles as $columnIndex => $columnStyle) { + $table->setColumnStyle($columnIndex, $columnStyle); + } + + $table->render(); + } + + /** + * Execute a given callback while advancing a progress bar. + * + * @param int|iterable $totalSteps + * @return mixed|void + */ + public function withProgressBar($totalSteps, Closure $callback) + { + $bar = $this->output->createProgressBar( + is_iterable($totalSteps) ? count($totalSteps) : $totalSteps + ); + + $bar->start(); + + if (is_iterable($totalSteps)) { + foreach ($totalSteps as $value) { + $callback($value, $bar); + + $bar->advance(); + } + } else { + $callback($bar); + } + + $bar->finish(); + + if (is_iterable($totalSteps)) { + return $totalSteps; + } + } + + /** + * Write a string as information output. + * + * @param string $string + * @param null|int|string $verbosity + */ + public function info($string, $verbosity = null) + { + $this->line($string, 'info', $verbosity); + } + + /** + * Write a string as standard output. + * + * @param string $string + * @param null|string $style + * @param null|int|string $verbosity + */ + public function line($string, $style = null, $verbosity = null) + { + $styled = $style ? "<{$style}>{$string}" : $string; + + $this->output->writeln($styled, $this->parseVerbosity($verbosity)); + } + + /** + * Write a string as comment output. + * + * @param string $string + * @param null|int|string $verbosity + */ + public function comment($string, $verbosity = null) + { + $this->line($string, 'comment', $verbosity); + } + + /** + * Write a string as question output. + * + * @param string $string + * @param null|int|string $verbosity + */ + public function question($string, $verbosity = null) + { + $this->line($string, 'question', $verbosity); + } + + /** + * Write a string as error output. + * + * @param string $string + * @param null|int|string $verbosity + */ + public function error($string, $verbosity = null) + { + $this->line($string, 'error', $verbosity); + } + + /** + * Write a string as warning output. + * + * @param string $string + * @param null|int|string $verbosity + */ + public function warn($string, $verbosity = null) + { + if (! $this->output->getFormatter()->hasStyle('warning')) { + $style = new OutputFormatterStyle('yellow'); + + $this->output->getFormatter()->setStyle('warning', $style); + } + + $this->line($string, 'warning', $verbosity); + } + + /** + * Write a string in an alert box. + * + * @param string $string + * @param null|int|string $verbosity + */ + public function alert($string, $verbosity = null) + { + $length = Str::length(strip_tags($string)) + 12; + + $this->comment(str_repeat('*', $length), $verbosity); + $this->comment('* ' . $string . ' *', $verbosity); + $this->comment(str_repeat('*', $length), $verbosity); + + $this->comment('', $verbosity); + } + + /** + * Write a blank line. + * + * @param int $count + * @return $this + */ + public function newLine($count = 1) + { + $this->output->newLine($count); + + return $this; + } + + /** + * Set the input interface implementation. + */ + public function setInput(InputInterface $input) + { + $this->input = $input; + } + + /** + * Set the output interface implementation. + */ + public function setOutput(OutputStyle $output) + { + $this->output = $output; + } + + /** + * Get the output implementation. + * + * @return OutputStyle + */ + public function getOutput() + { + return $this->output; + } + + /** + * Set the verbosity level. + * + * @param int|string $level + */ + protected function setVerbosity($level) + { + $this->verbosity = $this->parseVerbosity($level); + } + + /** + * Get the verbosity level in terms of Symfony's OutputInterface level. + * + * @param null|int|string $level + * @return int + */ + protected function parseVerbosity($level = null) + { + if (isset($this->verbosityMap[$level])) { + $level = $this->verbosityMap[$level]; + } elseif (! is_int($level)) { + $level = $this->verbosity; + } + + return $level; + } +}