diff --git a/Application.php b/Application.php
index 1a7e50388..78d885d25 100644
--- a/Application.php
+++ b/Application.php
@@ -21,6 +21,8 @@
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
+use Symfony\Component\Console\Completion\Suggestion;
+use Symfony\Component\Console\Event\ConsoleAlarmEvent;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleSignalEvent;
@@ -32,6 +34,7 @@
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\DebugFormatterHelper;
+use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Helper\HelperSet;
@@ -70,45 +73,45 @@
*/
class Application implements ResetInterface
{
- private $commands = [];
- private $wantHelps = false;
- private $runningCommand;
- private $name;
- private $version;
- private $commandLoader;
- private $catchExceptions = true;
- private $autoExit = true;
- private $definition;
- private $helperSet;
- private $dispatcher;
- private $terminal;
- private $defaultCommand;
- private $singleCommand = false;
- private $initialized;
- private $signalRegistry;
- private $signalsToDispatchEvent = [];
-
- public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
- {
- $this->name = $name;
- $this->version = $version;
+ private array $commands = [];
+ private bool $wantHelps = false;
+ private ?Command $runningCommand = null;
+ private ?CommandLoaderInterface $commandLoader = null;
+ private bool $catchExceptions = true;
+ private bool $catchErrors = false;
+ private bool $autoExit = true;
+ private InputDefinition $definition;
+ private HelperSet $helperSet;
+ private ?EventDispatcherInterface $dispatcher = null;
+ private Terminal $terminal;
+ private string $defaultCommand;
+ private bool $singleCommand = false;
+ private bool $initialized = false;
+ private ?SignalRegistry $signalRegistry = null;
+ private array $signalsToDispatchEvent = [];
+ private ?int $alarmInterval = null;
+
+ public function __construct(
+ private string $name = 'UNKNOWN',
+ private string $version = 'UNKNOWN',
+ ) {
$this->terminal = new Terminal();
$this->defaultCommand = 'list';
if (\defined('SIGINT') && SignalRegistry::isSupported()) {
$this->signalRegistry = new SignalRegistry();
- $this->signalsToDispatchEvent = [\SIGINT, \SIGTERM, \SIGUSR1, \SIGUSR2];
+ $this->signalsToDispatchEvent = [\SIGINT, \SIGQUIT, \SIGTERM, \SIGUSR1, \SIGUSR2, \SIGALRM];
}
}
/**
* @final
*/
- public function setDispatcher(EventDispatcherInterface $dispatcher)
+ public function setDispatcher(EventDispatcherInterface $dispatcher): void
{
$this->dispatcher = $dispatcher;
}
- public function setCommandLoader(CommandLoaderInterface $commandLoader)
+ public function setCommandLoader(CommandLoaderInterface $commandLoader): void
{
$this->commandLoader = $commandLoader;
}
@@ -116,17 +119,41 @@ public function setCommandLoader(CommandLoaderInterface $commandLoader)
public function getSignalRegistry(): SignalRegistry
{
if (!$this->signalRegistry) {
- throw new RuntimeException('Signals are not supported. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
+ throw new RuntimeException('Signals are not supported. Make sure that the "pcntl" extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
}
return $this->signalRegistry;
}
- public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent)
+ public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent): void
{
$this->signalsToDispatchEvent = $signalsToDispatchEvent;
}
+ /**
+ * Sets the interval to schedule a SIGALRM signal in seconds.
+ */
+ public function setAlarmInterval(?int $seconds): void
+ {
+ $this->alarmInterval = $seconds;
+ $this->scheduleAlarm();
+ }
+
+ /**
+ * Gets the interval in seconds on which a SIGALRM signal is dispatched.
+ */
+ public function getAlarmInterval(): ?int
+ {
+ return $this->alarmInterval;
+ }
+
+ private function scheduleAlarm(): void
+ {
+ if (null !== $this->alarmInterval) {
+ $this->getSignalRegistry()->scheduleAlarm($this->alarmInterval);
+ }
+ }
+
/**
* Runs the current application.
*
@@ -134,20 +161,15 @@ public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent)
*
* @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}.
*/
- public function run(?InputInterface $input = null, ?OutputInterface $output = null)
+ public function run(?InputInterface $input = null, ?OutputInterface $output = null): int
{
if (\function_exists('putenv')) {
@putenv('LINES='.$this->terminal->getHeight());
@putenv('COLUMNS='.$this->terminal->getWidth());
}
- if (null === $input) {
- $input = new ArgvInput();
- }
-
- if (null === $output) {
- $output = new ConsoleOutput();
- }
+ $input ??= new ArgvInput();
+ $output ??= new ConsoleOutput();
$renderException = function (\Throwable $e) use ($output) {
if ($output instanceof ConsoleOutputInterface) {
@@ -169,8 +191,11 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu
$this->configureIO($input, $output);
$exitCode = $this->doRun($input, $output);
- } catch (\Exception $e) {
- if (!$this->catchExceptions) {
+ } catch (\Throwable $e) {
+ if ($e instanceof \Exception && !$this->catchExceptions) {
+ throw $e;
+ }
+ if (!$e instanceof \Exception && !$this->catchErrors) {
throw $e;
}
@@ -217,7 +242,7 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu
*
* @return int 0 if everything went fine, or an error code
*/
- public function doRun(InputInterface $input, OutputInterface $output)
+ public function doRun(InputInterface $input, OutputInterface $output): int
{
if (true === $input->hasParameterOption(['--version', '-V'], true)) {
$output->writeln($this->getLongVersion());
@@ -228,7 +253,7 @@ public function doRun(InputInterface $input, OutputInterface $output)
try {
// Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument.
$input->bind($this->getDefinition());
- } catch (ExceptionInterface $e) {
+ } catch (ExceptionInterface) {
// Errors must be ignored, full binding/validation happens later when the command is known.
}
@@ -258,7 +283,26 @@ public function doRun(InputInterface $input, OutputInterface $output)
// the command name MUST be the first element of the input
$command = $this->find($name);
} catch (\Throwable $e) {
- if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) || 1 !== \count($alternatives = $e->getAlternatives()) || !$input->isInteractive()) {
+ if (($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) && 1 === \count($alternatives = $e->getAlternatives()) && $input->isInteractive()) {
+ $alternative = $alternatives[0];
+
+ $style = new SymfonyStyle($input, $output);
+ $output->writeln('');
+ $formattedBlock = (new FormatterHelper())->formatBlock(\sprintf('Command "%s" is not defined.', $name), 'error', true);
+ $output->writeln($formattedBlock);
+ if (!$style->confirm(\sprintf('Do you want to run "%s" instead? ', $alternative), false)) {
+ if (null !== $this->dispatcher) {
+ $event = new ConsoleErrorEvent($input, $output, $e);
+ $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
+
+ return $event->getExitCode();
+ }
+
+ return 1;
+ }
+
+ $command = $this->find($alternative);
+ } else {
if (null !== $this->dispatcher) {
$event = new ConsoleErrorEvent($input, $output, $e);
$this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
@@ -270,27 +314,24 @@ public function doRun(InputInterface $input, OutputInterface $output)
$e = $event->getError();
}
- throw $e;
- }
-
- $alternative = $alternatives[0];
-
- $style = new SymfonyStyle($input, $output);
- $output->writeln('');
- $formattedBlock = (new FormatterHelper())->formatBlock(sprintf('Command "%s" is not defined.', $name), 'error', true);
- $output->writeln($formattedBlock);
- if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) {
- if (null !== $this->dispatcher) {
- $event = new ConsoleErrorEvent($input, $output, $e);
- $this->dispatcher->dispatch($event, ConsoleEvents::ERROR);
+ try {
+ if ($e instanceof CommandNotFoundException && $namespace = $this->findNamespace($name)) {
+ $helper = new DescriptorHelper();
+ $helper->describe($output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output, $this, [
+ 'format' => 'txt',
+ 'raw_text' => false,
+ 'namespace' => $namespace,
+ 'short' => false,
+ ]);
+
+ return isset($event) ? $event->getExitCode() : 1;
+ }
- return $event->getExitCode();
+ throw $e;
+ } catch (NamespaceNotFoundException) {
+ throw $e;
}
-
- return 1;
}
-
- $command = $this->find($alternative);
}
if ($command instanceof LazyCommand) {
@@ -304,47 +345,34 @@ public function doRun(InputInterface $input, OutputInterface $output)
return $exitCode;
}
- /**
- * {@inheritdoc}
- */
- public function reset()
+ public function reset(): void
{
}
- public function setHelperSet(HelperSet $helperSet)
+ public function setHelperSet(HelperSet $helperSet): void
{
$this->helperSet = $helperSet;
}
/**
* Get the helper set associated with the command.
- *
- * @return HelperSet
*/
- public function getHelperSet()
+ public function getHelperSet(): HelperSet
{
- if (!$this->helperSet) {
- $this->helperSet = $this->getDefaultHelperSet();
- }
-
- return $this->helperSet;
+ return $this->helperSet ??= $this->getDefaultHelperSet();
}
- public function setDefinition(InputDefinition $definition)
+ public function setDefinition(InputDefinition $definition): void
{
$this->definition = $definition;
}
/**
* Gets the InputDefinition related to this Application.
- *
- * @return InputDefinition
*/
- public function getDefinition()
+ public function getDefinition(): InputDefinition
{
- if (!$this->definition) {
- $this->definition = $this->getDefaultInputDefinition();
- }
+ $this->definition ??= $this->getDefaultInputDefinition();
if ($this->singleCommand) {
$inputDefinition = $this->definition;
@@ -365,45 +393,37 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti
CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType()
&& 'command' === $input->getCompletionName()
) {
- $commandNames = [];
foreach ($this->all() as $name => $command) {
// skip hidden commands and aliased commands as they already get added below
if ($command->isHidden() || $command->getName() !== $name) {
continue;
}
- $commandNames[] = $command->getName();
+ $suggestions->suggestValue(new Suggestion($command->getName(), $command->getDescription()));
foreach ($command->getAliases() as $name) {
- $commandNames[] = $name;
+ $suggestions->suggestValue(new Suggestion($name, $command->getDescription()));
}
}
- $suggestions->suggestValues(array_filter($commandNames));
return;
}
if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) {
$suggestions->suggestOptions($this->getDefinition()->getOptions());
-
- return;
}
}
/**
* Gets the help message.
- *
- * @return string
*/
- public function getHelp()
+ public function getHelp(): string
{
return $this->getLongVersion();
}
/**
* Gets whether to catch exceptions or not during commands execution.
- *
- * @return bool
*/
- public function areExceptionsCaught()
+ public function areExceptionsCaught(): bool
{
return $this->catchExceptions;
}
@@ -411,17 +431,23 @@ public function areExceptionsCaught()
/**
* Sets whether to catch exceptions or not during commands execution.
*/
- public function setCatchExceptions(bool $boolean)
+ public function setCatchExceptions(bool $boolean): void
{
$this->catchExceptions = $boolean;
}
+ /**
+ * Sets whether to catch errors or not during commands execution.
+ */
+ public function setCatchErrors(bool $catchErrors = true): void
+ {
+ $this->catchErrors = $catchErrors;
+ }
+
/**
* Gets whether to automatically exit after a command execution or not.
- *
- * @return bool
*/
- public function isAutoExitEnabled()
+ public function isAutoExitEnabled(): bool
{
return $this->autoExit;
}
@@ -429,35 +455,31 @@ public function isAutoExitEnabled()
/**
* Sets whether to automatically exit after a command execution or not.
*/
- public function setAutoExit(bool $boolean)
+ public function setAutoExit(bool $boolean): void
{
$this->autoExit = $boolean;
}
/**
* Gets the name of the application.
- *
- * @return string
*/
- public function getName()
+ public function getName(): string
{
return $this->name;
}
/**
* Sets the application name.
- **/
- public function setName(string $name)
+ */
+ public function setName(string $name): void
{
$this->name = $name;
}
/**
* Gets the application version.
- *
- * @return string
*/
- public function getVersion()
+ public function getVersion(): string
{
return $this->version;
}
@@ -465,21 +487,19 @@ public function getVersion()
/**
* Sets the application version.
*/
- public function setVersion(string $version)
+ public function setVersion(string $version): void
{
$this->version = $version;
}
/**
* Returns the long version of the application.
- *
- * @return string
*/
- public function getLongVersion()
+ public function getLongVersion(): string
{
if ('UNKNOWN' !== $this->getName()) {
if ('UNKNOWN' !== $this->getVersion()) {
- return sprintf('%s %s', $this->getName(), $this->getVersion());
+ return \sprintf('%s %s', $this->getName(), $this->getVersion());
}
return $this->getName();
@@ -490,10 +510,8 @@ public function getLongVersion()
/**
* Registers a new command.
- *
- * @return Command
*/
- public function register(string $name)
+ public function register(string $name): Command
{
return $this->add(new Command($name));
}
@@ -505,7 +523,7 @@ public function register(string $name)
*
* @param Command[] $commands An array of commands
*/
- public function addCommands(array $commands)
+ public function addCommands(array $commands): void
{
foreach ($commands as $command) {
$this->add($command);
@@ -517,10 +535,8 @@ public function addCommands(array $commands)
*
* If a command with the same name already exists, it will be overridden.
* If the command is not enabled it will not be added.
- *
- * @return Command|null
*/
- public function add(Command $command)
+ public function add(Command $command): ?Command
{
$this->init();
@@ -538,7 +554,7 @@ public function add(Command $command)
}
if (!$command->getName()) {
- throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command)));
+ throw new LogicException(\sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command)));
}
$this->commands[$command->getName()] = $command;
@@ -553,21 +569,19 @@ public function add(Command $command)
/**
* Returns a registered command by name or alias.
*
- * @return Command
- *
* @throws CommandNotFoundException When given command name does not exist
*/
- public function get(string $name)
+ public function get(string $name): Command
{
$this->init();
if (!$this->has($name)) {
- throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
+ throw new CommandNotFoundException(\sprintf('The command "%s" does not exist.', $name));
}
// When the command has a different name than the one used at the command loader level
if (!isset($this->commands[$name])) {
- throw new CommandNotFoundException(sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name));
+ throw new CommandNotFoundException(\sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name));
}
$command = $this->commands[$name];
@@ -586,14 +600,12 @@ public function get(string $name)
/**
* Returns true if the command exists, false otherwise.
- *
- * @return bool
*/
- public function has(string $name)
+ public function has(string $name): bool
{
$this->init();
- return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name) && $this->add($this->commandLoader->get($name)));
+ return isset($this->commands[$name]) || ($this->commandLoader?->has($name) && $this->add($this->commandLoader->get($name)));
}
/**
@@ -603,7 +615,7 @@ public function has(string $name)
*
* @return string[]
*/
- public function getNamespaces()
+ public function getNamespaces(): array
{
$namespaces = [];
foreach ($this->all() as $command) {
@@ -624,18 +636,16 @@ public function getNamespaces()
/**
* Finds a registered namespace by a name or an abbreviation.
*
- * @return string
- *
* @throws NamespaceNotFoundException When namespace is incorrect or ambiguous
*/
- public function findNamespace(string $namespace)
+ public function findNamespace(string $namespace): string
{
$allNamespaces = $this->getNamespaces();
$expr = implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*';
$namespaces = preg_grep('{^'.$expr.'}', $allNamespaces);
- if (empty($namespaces)) {
- $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace);
+ if (!$namespaces) {
+ $message = \sprintf('There are no commands defined in the "%s" namespace.', $namespace);
if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) {
if (1 == \count($alternatives)) {
@@ -652,7 +662,7 @@ public function findNamespace(string $namespace)
$exact = \in_array($namespace, $namespaces, true);
if (\count($namespaces) > 1 && !$exact) {
- throw new NamespaceNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces));
+ throw new NamespaceNotFoundException(\sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces));
}
return $exact ? $namespace : reset($namespaces);
@@ -664,11 +674,9 @@ public function findNamespace(string $namespace)
* Contrary to get, this command tries to find the best
* match if you give it an abbreviation of a name or alias.
*
- * @return Command
- *
* @throws CommandNotFoundException When command name is incorrect or ambiguous
*/
- public function find(string $name)
+ public function find(string $name): Command
{
$this->init();
@@ -690,24 +698,22 @@ public function find(string $name)
$expr = implode('[^:]*:', array_map('preg_quote', explode(':', $name))).'[^:]*';
$commands = preg_grep('{^'.$expr.'}', $allCommands);
- if (empty($commands)) {
+ if (!$commands) {
$commands = preg_grep('{^'.$expr.'}i', $allCommands);
}
// if no commands matched or we just matched namespaces
- if (empty($commands) || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) {
+ if (!$commands || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) {
if (false !== $pos = strrpos($name, ':')) {
// check if a namespace exists and contains commands
$this->findNamespace(substr($name, 0, $pos));
}
- $message = sprintf('Command "%s" is not defined.', $name);
+ $message = \sprintf('Command "%s" is not defined.', $name);
if ($alternatives = $this->findAlternatives($name, $allCommands)) {
// remove hidden commands
- $alternatives = array_filter($alternatives, function ($name) {
- return !$this->get($name)->isHidden();
- });
+ $alternatives = array_filter($alternatives, fn ($name) => !$this->get($name)->isHidden());
if (1 == \count($alternatives)) {
$message .= "\n\nDid you mean this?\n ";
@@ -732,7 +738,7 @@ public function find(string $name)
$aliases[$nameOrAlias] = $commandName;
- return $commandName === $nameOrAlias || !\in_array($commandName, $commands);
+ return $commandName === $nameOrAlias || !\in_array($commandName, $commands, true);
}));
}
@@ -758,14 +764,14 @@ public function find(string $name)
if (\count($commands) > 1) {
$suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs));
- throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands));
+ throw new CommandNotFoundException(\sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands));
}
}
$command = $this->get(reset($commands));
if ($command->isHidden()) {
- throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
+ throw new CommandNotFoundException(\sprintf('The command "%s" does not exist.', $name));
}
return $command;
@@ -778,7 +784,7 @@ public function find(string $name)
*
* @return Command[]
*/
- public function all(?string $namespace = null)
+ public function all(?string $namespace = null): array
{
$this->init();
@@ -820,7 +826,7 @@ public function all(?string $namespace = null)
*
* @return string[][]
*/
- public static function getAbbreviations(array $names)
+ public static function getAbbreviations(array $names): array
{
$abbrevs = [];
foreach ($names as $name) {
@@ -840,7 +846,7 @@ public function renderThrowable(\Throwable $e, OutputInterface $output): void
$this->doRenderThrowable($e, $output);
if (null !== $this->runningCommand) {
- $output->writeln(sprintf('%s', OutputFormatter::escape(sprintf($this->runningCommand->getSynopsis(), $this->getName()))), OutputInterface::VERBOSITY_QUIET);
+ $output->writeln(\sprintf('%s', OutputFormatter::escape(\sprintf($this->runningCommand->getSynopsis(), $this->getName()))), OutputInterface::VERBOSITY_QUIET);
$output->writeln('', OutputInterface::VERBOSITY_QUIET);
}
}
@@ -851,16 +857,14 @@ protected function doRenderThrowable(\Throwable $e, OutputInterface $output): vo
$message = trim($e->getMessage());
if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
$class = get_debug_type($e);
- $title = sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
+ $title = \sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
$len = Helper::width($title);
} else {
$len = 0;
}
if (str_contains($message, "@anonymous\0")) {
- $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', function ($m) {
- return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
- }, $message);
+ $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', fn ($m) => class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0], $message);
}
$width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX;
@@ -877,14 +881,14 @@ protected function doRenderThrowable(\Throwable $e, OutputInterface $output): vo
$messages = [];
if (!$e instanceof ExceptionInterface || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
- $messages[] = sprintf('%s', OutputFormatter::escape(sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a')));
+ $messages[] = \sprintf('%s', OutputFormatter::escape(\sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a')));
}
- $messages[] = $emptyLine = sprintf('%s', str_repeat(' ', $len));
+ $messages[] = $emptyLine = \sprintf('%s', str_repeat(' ', $len));
if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
- $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - Helper::width($title))));
+ $messages[] = \sprintf('%s%s', $title, str_repeat(' ', max(0, $len - Helper::width($title))));
}
foreach ($lines as $line) {
- $messages[] = sprintf(' %s %s', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1]));
+ $messages[] = \sprintf(' %s %s', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1]));
}
$messages[] = $emptyLine;
$messages[] = '';
@@ -911,7 +915,7 @@ protected function doRenderThrowable(\Throwable $e, OutputInterface $output): vo
$file = $trace[$i]['file'] ?? 'n/a';
$line = $trace[$i]['line'] ?? 'n/a';
- $output->writeln(sprintf(' %s%s at %s:%s', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET);
+ $output->writeln(\sprintf(' %s%s at %s:%s', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET);
}
$output->writeln('', OutputInterface::VERBOSITY_QUIET);
@@ -922,7 +926,7 @@ protected function doRenderThrowable(\Throwable $e, OutputInterface $output): vo
/**
* Configures the input and output instances based on the user arguments and options.
*/
- protected function configureIO(InputInterface $input, OutputInterface $output)
+ protected function configureIO(InputInterface $input, OutputInterface $output): void
{
if (true === $input->hasParameterOption(['--ansi'], true)) {
$output->setDecorated(true);
@@ -935,6 +939,9 @@ protected function configureIO(InputInterface $input, OutputInterface $output)
}
switch ($shellVerbosity = (int) getenv('SHELL_VERBOSITY')) {
+ case -2:
+ $output->setVerbosity(OutputInterface::VERBOSITY_SILENT);
+ break;
case -1:
$output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
break;
@@ -952,7 +959,10 @@ protected function configureIO(InputInterface $input, OutputInterface $output)
break;
}
- if (true === $input->hasParameterOption(['--quiet', '-q'], true)) {
+ if (true === $input->hasParameterOption(['--silent'], true)) {
+ $output->setVerbosity(OutputInterface::VERBOSITY_SILENT);
+ $shellVerbosity = -2;
+ } elseif (true === $input->hasParameterOption(['--quiet', '-q'], true)) {
$output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
$shellVerbosity = -1;
} else {
@@ -968,7 +978,7 @@ protected function configureIO(InputInterface $input, OutputInterface $output)
}
}
- if (-1 === $shellVerbosity) {
+ if (0 > $shellVerbosity) {
$input->setInteractive(false);
}
@@ -987,7 +997,7 @@ protected function configureIO(InputInterface $input, OutputInterface $output)
*
* @return int 0 if everything went fine, or an error code
*/
- protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
+ protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output): int
{
foreach ($command->getHelperSet() as $helper) {
if ($helper instanceof InputAwareInterface) {
@@ -995,44 +1005,70 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
}
}
- if ($this->signalsToDispatchEvent) {
- $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
-
- if ($commandSignals || null !== $this->dispatcher) {
- if (!$this->signalRegistry) {
- throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
- }
+ $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
+ if ($commandSignals || $this->dispatcher && $this->signalsToDispatchEvent) {
+ $signalRegistry = $this->getSignalRegistry();
- if (Terminal::hasSttyAvailable()) {
- $sttyMode = shell_exec('stty -g');
+ if (Terminal::hasSttyAvailable()) {
+ $sttyMode = shell_exec('stty -g');
- foreach ([\SIGINT, \SIGTERM] as $signal) {
- $this->signalRegistry->register($signal, static function () use ($sttyMode) {
- shell_exec('stty '.$sttyMode);
- });
- }
+ foreach ([\SIGINT, \SIGQUIT, \SIGTERM] as $signal) {
+ $signalRegistry->register($signal, static fn () => shell_exec('stty '.$sttyMode));
}
}
- if (null !== $this->dispatcher) {
+ if ($this->dispatcher) {
+ // We register application signals, so that we can dispatch the event
foreach ($this->signalsToDispatchEvent as $signal) {
- $event = new ConsoleSignalEvent($command, $input, $output, $signal);
+ $signalEvent = new ConsoleSignalEvent($command, $input, $output, $signal);
+ $alarmEvent = \SIGALRM === $signal ? new ConsoleAlarmEvent($command, $input, $output) : null;
+
+ $signalRegistry->register($signal, function ($signal) use ($signalEvent, $alarmEvent, $command, $commandSignals, $input, $output) {
+ $this->dispatcher->dispatch($signalEvent, ConsoleEvents::SIGNAL);
+ $exitCode = $signalEvent->getExitCode();
+
+ if (null !== $alarmEvent) {
+ if (false !== $exitCode) {
+ $alarmEvent->setExitCode($exitCode);
+ } else {
+ $alarmEvent->abortExit();
+ }
+ $this->dispatcher->dispatch($alarmEvent);
+ $exitCode = $alarmEvent->getExitCode();
+ }
- $this->signalRegistry->register($signal, function ($signal, $hasNext) use ($event) {
- $this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
+ // If the command is signalable, we call the handleSignal() method
+ if (\in_array($signal, $commandSignals, true)) {
+ $exitCode = $command->handleSignal($signal, $exitCode);
+ }
- // No more handlers, we try to simulate PHP default behavior
- if (!$hasNext) {
- if (!\in_array($signal, [\SIGUSR1, \SIGUSR2], true)) {
- exit(0);
- }
+ if (\SIGALRM === $signal) {
+ $this->scheduleAlarm();
+ }
+
+ if (false !== $exitCode) {
+ $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode, $signal);
+ $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE);
+
+ exit($event->getExitCode());
}
});
}
+
+ // then we register command signals, but not if already handled after the dispatcher
+ $commandSignals = array_diff($commandSignals, $this->signalsToDispatchEvent);
}
foreach ($commandSignals as $signal) {
- $this->signalRegistry->register($signal, [$command, 'handleSignal']);
+ $signalRegistry->register($signal, function (int $signal) use ($command): void {
+ if (\SIGALRM === $signal) {
+ $this->scheduleAlarm();
+ }
+
+ if (false !== $exitCode = $command->handleSignal($signal)) {
+ exit($exitCode);
+ }
+ });
}
}
@@ -1044,7 +1080,7 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
try {
$command->mergeApplicationDefinition();
$input->bind($command->getDefinition());
- } catch (ExceptionInterface $e) {
+ } catch (ExceptionInterface) {
// ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
}
@@ -1081,25 +1117,22 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
/**
* Gets the name of the command based on input.
- *
- * @return string|null
*/
- protected function getCommandName(InputInterface $input)
+ protected function getCommandName(InputInterface $input): ?string
{
return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument();
}
/**
* Gets the default input definition.
- *
- * @return InputDefinition
*/
- protected function getDefaultInputDefinition()
+ protected function getDefaultInputDefinition(): InputDefinition
{
return new InputDefinition([
new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display help for the given command. When no command is given display help for the '.$this->defaultCommand.' command'),
- new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
+ new InputOption('--silent', null, InputOption::VALUE_NONE, 'Do not output any message'),
+ new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Only errors are displayed. All other output is suppressed'),
new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'),
new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'),
new InputOption('--ansi', '', InputOption::VALUE_NEGATABLE, 'Force (or disable --no-ansi) ANSI output', null),
@@ -1112,17 +1145,15 @@ protected function getDefaultInputDefinition()
*
* @return Command[]
*/
- protected function getDefaultCommands()
+ protected function getDefaultCommands(): array
{
return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()];
}
/**
* Gets the default helper set with the helpers that should always be available.
- *
- * @return HelperSet
*/
- protected function getDefaultHelperSet()
+ protected function getDefaultHelperSet(): HelperSet
{
return new HelperSet([
new FormatterHelper(),
@@ -1144,10 +1175,8 @@ private function getAbbreviationSuggestions(array $abbrevs): string
* Returns the namespace part of the command name.
*
* This method is not part of public API and should not be used directly.
- *
- * @return string
*/
- public function extractNamespace(string $name, ?int $limit = null)
+ public function extractNamespace(string $name, ?int $limit = null): string
{
$parts = explode(':', $name, -1);
@@ -1196,7 +1225,7 @@ private function findAlternatives(string $name, iterable $collection): array
}
}
- $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
+ $alternatives = array_filter($alternatives, fn ($lev) => $lev < 2 * $threshold);
ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE);
return array_keys($alternatives);
@@ -1207,7 +1236,7 @@ private function findAlternatives(string $name, iterable $collection): array
*
* @return $this
*/
- public function setDefaultCommand(string $commandName, bool $isSingleCommand = false)
+ public function setDefaultCommand(string $commandName, bool $isSingleCommand = false): static
{
$this->defaultCommand = explode('|', ltrim($commandName, '|'))[0];
@@ -1287,7 +1316,7 @@ private function extractAllNamespaces(string $name): array
return $namespaces;
}
- private function init()
+ private function init(): void
{
if ($this->initialized) {
return;
diff --git a/Attribute/AsCommand.php b/Attribute/AsCommand.php
index b337f548f..6066d7c53 100644
--- a/Attribute/AsCommand.php
+++ b/Attribute/AsCommand.php
@@ -17,6 +17,12 @@
#[\Attribute(\Attribute::TARGET_CLASS)]
class AsCommand
{
+ /**
+ * @param string $name The name of the command, used when calling it (i.e. "cache:clear")
+ * @param string|null $description The description of the command, displayed with the help page
+ * @param string[] $aliases The list of aliases of the command. The command will be executed when using one of them (i.e. "cache:clean")
+ * @param bool $hidden If true, the command won't be shown when listing all the available commands, but it can still be run as any other command
+ */
public function __construct(
public string $name,
public ?string $description = null,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6662dd1eb..2c963568c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,73 @@
CHANGELOG
=========
+7.2
+---
+
+ * Add support for `FORCE_COLOR` environment variable
+ * Add `verbosity` argument to `mustRun` process helper method
+ * [BC BREAK] Add silent verbosity (`--silent`/`SHELL_VERBOSITY=-2`) to suppress all output, including errors
+ * Add `OutputInterface::isSilent()`, `Output::isSilent()`, `OutputStyle::isSilent()` methods
+ * Add a configurable finished indicator to the progress indicator to show that the progress is finished
+ * Add ability to schedule alarm signals and a `ConsoleAlarmEvent`
+
+7.1
+---
+
+ * Add `ArgvInput::getRawTokens()`
+
+7.0
+---
+
+ * Add method `__toString()` to `InputInterface`
+ * Remove `Command::$defaultName` and `Command::$defaultDescription`, use the `AsCommand` attribute instead
+ * Require explicit argument when calling `*Command::setApplication()`, `*FormatterStyle::setForeground/setBackground()`, `Helper::setHelpSet()`, `Input*::setDefault()` and `Question::setAutocompleterCallback/setValidator()`
+ * Remove `StringInput::REGEX_STRING`
+
+6.4
+---
+
+ * Add `SignalMap` to map signal value to its name
+ * Multi-line text in vertical tables is aligned properly
+ * The application can also catch errors with `Application::setCatchErrors(true)`
+ * Add `RunCommandMessage` and `RunCommandMessageHandler`
+ * Dispatch `ConsoleTerminateEvent` after an exit on signal handling and add `ConsoleTerminateEvent::getInterruptingSignal()`
+
+6.3
+---
+
+ * Add support for choosing exit code while handling signal, or to not exit at all
+ * Add `ProgressBar::setPlaceholderFormatter` to set a placeholder attached to a instance, instead of being global.
+ * Add `ReStructuredTextDescriptor`
+
+6.2
+---
+
+ * Improve truecolor terminal detection in some cases
+ * Add support for 256 color terminals (conversion from Ansi24 to Ansi8 if terminal is capable of it)
+ * Deprecate calling `*Command::setApplication()`, `*FormatterStyle::setForeground/setBackground()`, `Helper::setHelpSet()`, `Input*::setDefault()`, `Question::setAutocompleterCallback/setValidator()`without any arguments
+ * Change the signature of `OutputFormatterStyleInterface::setForeground/setBackground()` to `setForeground/setBackground(?string)`
+ * Change the signature of `HelperInterface::setHelperSet()` to `setHelperSet(?HelperSet)`
+
+6.1
+---
+
+ * Add support to display table vertically when calling setVertical()
+ * Add method `__toString()` to `InputInterface`
+ * Added `OutputWrapper` to prevent truncated URL in `SymfonyStyle::createBlock`.
+ * Deprecate `Command::$defaultName` and `Command::$defaultDescription`, use the `AsCommand` attribute instead
+ * Add suggested values for arguments and options in input definition, for input completion
+ * Add `$resumeAt` parameter to `ProgressBar#start()`, so that one can easily 'resume' progress on longer tasks, and still get accurate `getEstimate()` and `getRemaining()` results.
+
+6.0
+---
+
+ * `Command::setHidden()` has a default value (`true`) for `$hidden` parameter and is final
+ * Remove `Helper::strlen()`, use `Helper::width()` instead
+ * Remove `Helper::strlenWithoutDecoration()`, use `Helper::removeDecoration()` instead
+ * `AddConsoleCommandPass` can not be configured anymore
+ * Remove `HelperSet::setCommand()` and `getCommand()` without replacement
+
5.4
---
diff --git a/CI/GithubActionReporter.php b/CI/GithubActionReporter.php
index 065717854..952d380d5 100644
--- a/CI/GithubActionReporter.php
+++ b/CI/GithubActionReporter.php
@@ -20,8 +20,6 @@
*/
class GithubActionReporter
{
- private $output;
-
/**
* @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L80-L85
*/
@@ -42,9 +40,9 @@ class GithubActionReporter
',' => '%2C',
];
- public function __construct(OutputInterface $output)
- {
- $this->output = $output;
+ public function __construct(
+ private OutputInterface $output,
+ ) {
}
public static function isGithubActionEnvironment(): bool
@@ -89,11 +87,11 @@ private function log(string $type, string $message, ?string $file = null, ?int $
if (!$file) {
// No file provided, output the message solely:
- $this->output->writeln(sprintf('::%s::%s', $type, $message));
+ $this->output->writeln(\sprintf('::%s::%s', $type, $message));
return;
}
- $this->output->writeln(sprintf('::%s file=%s,line=%s,col=%s::%s', $type, strtr($file, self::ESCAPED_PROPERTIES), strtr($line ?? 1, self::ESCAPED_PROPERTIES), strtr($col ?? 0, self::ESCAPED_PROPERTIES), $message));
+ $this->output->writeln(\sprintf('::%s file=%s,line=%s,col=%s::%s', $type, strtr($file, self::ESCAPED_PROPERTIES), strtr($line ?? 1, self::ESCAPED_PROPERTIES), strtr($col ?? 0, self::ESCAPED_PROPERTIES), $message));
}
}
diff --git a/Color.php b/Color.php
index 22a4ce9ff..b1914c19a 100644
--- a/Color.php
+++ b/Color.php
@@ -49,9 +49,9 @@ final class Color
'conceal' => ['set' => 8, 'unset' => 28],
];
- private $foreground;
- private $background;
- private $options = [];
+ private string $foreground;
+ private string $background;
+ private array $options = [];
public function __construct(string $foreground = '', string $background = '', array $options = [])
{
@@ -60,7 +60,7 @@ public function __construct(string $foreground = '', string $background = '', ar
foreach ($options as $option) {
if (!isset(self::AVAILABLE_OPTIONS[$option])) {
- throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS))));
+ throw new InvalidArgumentException(\sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS))));
}
$this->options[$option] = self::AVAILABLE_OPTIONS[$option];
@@ -88,7 +88,7 @@ public function set(): string
return '';
}
- return sprintf("\033[%sm", implode(';', $setCodes));
+ return \sprintf("\033[%sm", implode(';', $setCodes));
}
public function unset(): string
@@ -107,7 +107,7 @@ public function unset(): string
return '';
}
- return sprintf("\033[%sm", implode(';', $unsetCodes));
+ return \sprintf("\033[%sm", implode(';', $unsetCodes));
}
private function parseColor(string $color, bool $background = false): string
@@ -117,17 +117,7 @@ private function parseColor(string $color, bool $background = false): string
}
if ('#' === $color[0]) {
- $color = substr($color, 1);
-
- if (3 === \strlen($color)) {
- $color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2];
- }
-
- if (6 !== \strlen($color)) {
- throw new InvalidArgumentException(sprintf('Invalid "%s" color.', $color));
- }
-
- return ($background ? '4' : '3').$this->convertHexColorToAnsi(hexdec($color));
+ return ($background ? '4' : '3').Terminal::getColorMode()->convertFromHexToAnsiColorCode($color);
}
if (isset(self::COLORS[$color])) {
@@ -138,43 +128,6 @@ private function parseColor(string $color, bool $background = false): string
return ($background ? '10' : '9').self::BRIGHT_COLORS[$color];
}
- throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS)))));
- }
-
- private function convertHexColorToAnsi(int $color): string
- {
- $r = ($color >> 16) & 255;
- $g = ($color >> 8) & 255;
- $b = $color & 255;
-
- // see https://github.com/termstandard/colors/ for more information about true color support
- if ('truecolor' !== getenv('COLORTERM')) {
- return (string) $this->degradeHexColorToAnsi($r, $g, $b);
- }
-
- return sprintf('8;2;%d;%d;%d', $r, $g, $b);
- }
-
- private function degradeHexColorToAnsi(int $r, int $g, int $b): int
- {
- if (0 === round($this->getSaturation($r, $g, $b) / 50)) {
- return 0;
- }
-
- return (round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255);
- }
-
- private function getSaturation(int $r, int $g, int $b): int
- {
- $r = $r / 255;
- $g = $g / 255;
- $b = $b / 255;
- $v = max($r, $g, $b);
-
- if (0 === $diff = $v - min($r, $g, $b)) {
- return 0;
- }
-
- return (int) $diff * 100 / $v;
+ throw new InvalidArgumentException(\sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS)))));
}
}
diff --git a/Command/Command.php b/Command/Command.php
index d18103670..244a419f2 100644
--- a/Command/Command.php
+++ b/Command/Command.php
@@ -15,9 +15,11 @@
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
+use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
+use Symfony\Component\Console\Helper\HelperInterface;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
@@ -37,58 +39,37 @@ class Command
public const FAILURE = 1;
public const INVALID = 2;
- /**
- * @var string|null The default command name
- */
- protected static $defaultName;
-
- /**
- * @var string|null The default command description
- */
- protected static $defaultDescription;
-
- private $application;
- private $name;
- private $processTitle;
- private $aliases = [];
- private $definition;
- private $hidden = false;
- private $help = '';
- private $description = '';
- private $fullDefinition;
- private $ignoreValidationErrors = false;
- private $code;
- private $synopsis = [];
- private $usages = [];
- private $helperSet;
-
- /**
- * @return string|null
- */
- public static function getDefaultName()
- {
- $class = static::class;
-
- if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
+ private ?Application $application = null;
+ private ?string $name = null;
+ private ?string $processTitle = null;
+ private array $aliases = [];
+ private InputDefinition $definition;
+ private bool $hidden = false;
+ private string $help = '';
+ private string $description = '';
+ private ?InputDefinition $fullDefinition = null;
+ private bool $ignoreValidationErrors = false;
+ private ?\Closure $code = null;
+ private array $synopsis = [];
+ private array $usages = [];
+ private ?HelperSet $helperSet = null;
+
+ public static function getDefaultName(): ?string
+ {
+ if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) {
return $attribute[0]->newInstance()->name;
}
- $r = new \ReflectionProperty($class, 'defaultName');
-
- return $class === $r->class ? static::$defaultName : null;
+ return null;
}
public static function getDefaultDescription(): ?string
{
- $class = static::class;
-
- if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
+ if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) {
return $attribute[0]->newInstance()->description;
}
- $r = new \ReflectionProperty($class, 'defaultDescription');
-
- return $class === $r->class ? static::$defaultDescription : null;
+ return null;
}
/**
@@ -127,12 +108,12 @@ public function __construct(?string $name = null)
*
* This is mainly useful for the help command.
*/
- public function ignoreValidationErrors()
+ public function ignoreValidationErrors(): void
{
$this->ignoreValidationErrors = true;
}
- public function setApplication(?Application $application = null)
+ public function setApplication(?Application $application): void
{
$this->application = $application;
if ($application) {
@@ -144,27 +125,23 @@ public function setApplication(?Application $application = null)
$this->fullDefinition = null;
}
- public function setHelperSet(HelperSet $helperSet)
+ public function setHelperSet(HelperSet $helperSet): void
{
$this->helperSet = $helperSet;
}
/**
* Gets the helper set.
- *
- * @return HelperSet|null
*/
- public function getHelperSet()
+ public function getHelperSet(): ?HelperSet
{
return $this->helperSet;
}
/**
* Gets the application instance for this command.
- *
- * @return Application|null
*/
- public function getApplication()
+ public function getApplication(): ?Application
{
return $this->application;
}
@@ -174,16 +151,16 @@ public function getApplication()
*
* Override this to check for x or y and return false if the command cannot
* run properly under the current conditions.
- *
- * @return bool
*/
- public function isEnabled()
+ public function isEnabled(): bool
{
return true;
}
/**
* Configures the current command.
+ *
+ * @return void
*/
protected function configure()
{
@@ -203,7 +180,7 @@ protected function configure()
*
* @see setCode()
*/
- protected function execute(InputInterface $input, OutputInterface $output)
+ protected function execute(InputInterface $input, OutputInterface $output): int
{
throw new LogicException('You must override the execute() method in the concrete command class.');
}
@@ -214,6 +191,8 @@ protected function execute(InputInterface $input, OutputInterface $output)
* This method is executed before the InputDefinition is validated.
* This means that this is the only place where the command can
* interactively ask for values of missing required arguments.
+ *
+ * @return void
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
@@ -228,6 +207,8 @@ protected function interact(InputInterface $input, OutputInterface $output)
*
* @see InputInterface::bind()
* @see InputInterface::validate()
+ *
+ * @return void
*/
protected function initialize(InputInterface $input, OutputInterface $output)
{
@@ -247,7 +228,7 @@ protected function initialize(InputInterface $input, OutputInterface $output)
* @see setCode()
* @see execute()
*/
- public function run(InputInterface $input, OutputInterface $output)
+ public function run(InputInterface $input, OutputInterface $output): int
{
// add the application arguments and options
$this->mergeApplicationDefinition();
@@ -296,20 +277,22 @@ public function run(InputInterface $input, OutputInterface $output)
$statusCode = ($this->code)($input, $output);
} else {
$statusCode = $this->execute($input, $output);
-
- if (!\is_int($statusCode)) {
- throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode)));
- }
}
return is_numeric($statusCode) ? (int) $statusCode : 0;
}
/**
- * Adds suggestions to $suggestions for the current completion input (e.g. option or argument).
+ * Supplies suggestions when resolving possible completion options for input (e.g. option or argument).
*/
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
+ $definition = $this->getDefinition();
+ if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() && $definition->hasOption($input->getCompletionName())) {
+ $definition->getOption($input->getCompletionName())->complete($input, $suggestions);
+ } elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && $definition->hasArgument($input->getCompletionName())) {
+ $definition->getArgument($input->getCompletionName())->complete($input, $suggestions);
+ }
}
/**
@@ -326,7 +309,7 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti
*
* @see execute()
*/
- public function setCode(callable $code)
+ public function setCode(callable $code): static
{
if ($code instanceof \Closure) {
$r = new \ReflectionFunction($code);
@@ -340,6 +323,8 @@ public function setCode(callable $code)
restore_error_handler();
}
}
+ } else {
+ $code = $code(...);
}
$this->code = $code;
@@ -356,7 +341,7 @@ public function setCode(callable $code)
*
* @internal
*/
- public function mergeApplicationDefinition(bool $mergeArgs = true)
+ public function mergeApplicationDefinition(bool $mergeArgs = true): void
{
if (null === $this->application) {
return;
@@ -377,11 +362,9 @@ public function mergeApplicationDefinition(bool $mergeArgs = true)
/**
* Sets an array of argument and option instances.
*
- * @param array|InputDefinition $definition An array of argument and option instances or a definition instance
- *
* @return $this
*/
- public function setDefinition($definition)
+ public function setDefinition(array|InputDefinition $definition): static
{
if ($definition instanceof InputDefinition) {
$this->definition = $definition;
@@ -396,10 +379,8 @@ public function setDefinition($definition)
/**
* Gets the InputDefinition attached to this Command.
- *
- * @return InputDefinition
*/
- public function getDefinition()
+ public function getDefinition(): InputDefinition
{
return $this->fullDefinition ?? $this->getNativeDefinition();
}
@@ -411,34 +392,27 @@ public function getDefinition()
* be changed by merging with the application InputDefinition.
*
* This method is not part of public API and should not be used directly.
- *
- * @return InputDefinition
*/
- public function getNativeDefinition()
+ public function getNativeDefinition(): InputDefinition
{
- if (null === $this->definition) {
- throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
- }
-
- return $this->definition;
+ return $this->definition ?? throw new LogicException(\sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
}
/**
* Adds an argument.
*
- * @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
- * @param mixed $default The default value (for InputArgument::OPTIONAL mode only)
+ * @param $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
+ * @param $default The default value (for InputArgument::OPTIONAL mode only)
+ * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion
*
* @return $this
*
* @throws InvalidArgumentException When argument mode is not valid
*/
- public function addArgument(string $name, ?int $mode = null, string $description = '', $default = null)
+ public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
{
- $this->definition->addArgument(new InputArgument($name, $mode, $description, $default));
- if (null !== $this->fullDefinition) {
- $this->fullDefinition->addArgument(new InputArgument($name, $mode, $description, $default));
- }
+ $this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
+ $this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
return $this;
}
@@ -446,20 +420,19 @@ public function addArgument(string $name, ?int $mode = null, string $description
/**
* Adds an option.
*
- * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
- * @param int|null $mode The option mode: One of the InputOption::VALUE_* constants
- * @param mixed $default The default value (must be null for InputOption::VALUE_NONE)
+ * @param $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
+ * @param $mode The option mode: One of the InputOption::VALUE_* constants
+ * @param $default The default value (must be null for InputOption::VALUE_NONE)
+ * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion
*
* @return $this
*
* @throws InvalidArgumentException If option mode is invalid or incompatible
*/
- public function addOption(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null)
+ public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
{
- $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
- if (null !== $this->fullDefinition) {
- $this->fullDefinition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
- }
+ $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
+ $this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
return $this;
}
@@ -476,7 +449,7 @@ public function addOption(string $name, $shortcut = null, ?int $mode = null, str
*
* @throws InvalidArgumentException When the name is invalid
*/
- public function setName(string $name)
+ public function setName(string $name): static
{
$this->validateName($name);
@@ -493,7 +466,7 @@ public function setName(string $name)
*
* @return $this
*/
- public function setProcessTitle(string $title)
+ public function setProcessTitle(string $title): static
{
$this->processTitle = $title;
@@ -502,23 +475,18 @@ public function setProcessTitle(string $title)
/**
* Returns the command name.
- *
- * @return string|null
*/
- public function getName()
+ public function getName(): ?string
{
return $this->name;
}
/**
* @param bool $hidden Whether or not the command should be hidden from the list of commands
- * The default value will be true in Symfony 6.0
*
* @return $this
- *
- * @final since Symfony 5.1
*/
- public function setHidden(bool $hidden /* = true */)
+ public function setHidden(bool $hidden = true): static
{
$this->hidden = $hidden;
@@ -528,7 +496,7 @@ public function setHidden(bool $hidden /* = true */)
/**
* @return bool whether the command should be publicly shown or not
*/
- public function isHidden()
+ public function isHidden(): bool
{
return $this->hidden;
}
@@ -538,7 +506,7 @@ public function isHidden()
*
* @return $this
*/
- public function setDescription(string $description)
+ public function setDescription(string $description): static
{
$this->description = $description;
@@ -547,10 +515,8 @@ public function setDescription(string $description)
/**
* Returns the description for the command.
- *
- * @return string
*/
- public function getDescription()
+ public function getDescription(): string
{
return $this->description;
}
@@ -560,7 +526,7 @@ public function getDescription()
*
* @return $this
*/
- public function setHelp(string $help)
+ public function setHelp(string $help): static
{
$this->help = $help;
@@ -569,10 +535,8 @@ public function setHelp(string $help)
/**
* Returns the help for the command.
- *
- * @return string
*/
- public function getHelp()
+ public function getHelp(): string
{
return $this->help;
}
@@ -580,13 +544,11 @@ public function getHelp()
/**
* Returns the processed help for the command replacing the %command.name% and
* %command.full_name% patterns with the real values dynamically.
- *
- * @return string
*/
- public function getProcessedHelp()
+ public function getProcessedHelp(): string
{
$name = $this->name;
- $isSingleCommand = $this->application && $this->application->isSingleCommand();
+ $isSingleCommand = $this->application?->isSingleCommand();
$placeholders = [
'%command.name%',
@@ -609,7 +571,7 @@ public function getProcessedHelp()
*
* @throws InvalidArgumentException When an alias is invalid
*/
- public function setAliases(iterable $aliases)
+ public function setAliases(iterable $aliases): static
{
$list = [];
@@ -625,10 +587,8 @@ public function setAliases(iterable $aliases)
/**
* Returns the aliases for the command.
- *
- * @return array
*/
- public function getAliases()
+ public function getAliases(): array
{
return $this->aliases;
}
@@ -637,15 +597,13 @@ public function getAliases()
* Returns the synopsis for the command.
*
* @param bool $short Whether to show the short version of the synopsis (with options folded) or not
- *
- * @return string
*/
- public function getSynopsis(bool $short = false)
+ public function getSynopsis(bool $short = false): string
{
$key = $short ? 'short' : 'long';
if (!isset($this->synopsis[$key])) {
- $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
+ $this->synopsis[$key] = trim(\sprintf('%s %s', $this->name, $this->definition->getSynopsis($short)));
}
return $this->synopsis[$key];
@@ -656,10 +614,10 @@ public function getSynopsis(bool $short = false)
*
* @return $this
*/
- public function addUsage(string $usage)
+ public function addUsage(string $usage): static
{
if (!str_starts_with($usage, $this->name)) {
- $usage = sprintf('%s %s', $this->name, $usage);
+ $usage = \sprintf('%s %s', $this->name, $usage);
}
$this->usages[] = $usage;
@@ -669,10 +627,8 @@ public function addUsage(string $usage)
/**
* Returns alternative usages of the command.
- *
- * @return array
*/
- public function getUsages()
+ public function getUsages(): array
{
return $this->usages;
}
@@ -680,15 +636,13 @@ public function getUsages()
/**
* Gets a helper instance by name.
*
- * @return mixed
- *
* @throws LogicException if no HelperSet is defined
* @throws InvalidArgumentException if the helper is not defined
*/
- public function getHelper(string $name)
+ public function getHelper(string $name): HelperInterface
{
if (null === $this->helperSet) {
- throw new LogicException(sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name));
+ throw new LogicException(\sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name));
}
return $this->helperSet->get($name);
@@ -701,10 +655,10 @@ public function getHelper(string $name)
*
* @throws InvalidArgumentException When the name is invalid
*/
- private function validateName(string $name)
+ private function validateName(string $name): void
{
if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
- throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));
+ throw new InvalidArgumentException(\sprintf('Command name "%s" is invalid.', $name));
}
}
}
diff --git a/Command/CompleteCommand.php b/Command/CompleteCommand.php
index 0e35143c3..15eeea16a 100644
--- a/Command/CompleteCommand.php
+++ b/Command/CompleteCommand.php
@@ -11,10 +11,13 @@
namespace Symfony\Component\Console\Command;
+use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Output\BashCompletionOutput;
use Symfony\Component\Console\Completion\Output\CompletionOutputInterface;
+use Symfony\Component\Console\Completion\Output\FishCompletionOutput;
+use Symfony\Component\Console\Completion\Output\ZshCompletionOutput;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Input\InputInterface;
@@ -26,14 +29,13 @@
*
* @author Wouter de Jong
*/
+#[AsCommand(name: '|_complete', description: 'Internal command to provide shell completion suggestions')]
final class CompleteCommand extends Command
{
- protected static $defaultName = '|_complete';
- protected static $defaultDescription = 'Internal command to provide shell completion suggestions';
+ public const COMPLETION_API_VERSION = '1';
- private $completionOutputs;
-
- private $isDebug = false;
+ private array $completionOutputs;
+ private bool $isDebug = false;
/**
* @param array> $completionOutputs A list of additional completion outputs, with shell name as key and FQCN as value
@@ -41,7 +43,11 @@ final class CompleteCommand extends Command
public function __construct(array $completionOutputs = [])
{
// must be set before the parent constructor, as the property value is used in configure()
- $this->completionOutputs = $completionOutputs + ['bash' => BashCompletionOutput::class];
+ $this->completionOutputs = $completionOutputs + [
+ 'bash' => BashCompletionOutput::class,
+ 'fish' => FishCompletionOutput::class,
+ 'zsh' => ZshCompletionOutput::class,
+ ];
parent::__construct();
}
@@ -52,28 +58,29 @@ protected function configure(): void
->addOption('shell', 's', InputOption::VALUE_REQUIRED, 'The shell type ("'.implode('", "', array_keys($this->completionOutputs)).'")')
->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'An array of input tokens (e.g. COMP_WORDS or argv)')
->addOption('current', 'c', InputOption::VALUE_REQUIRED, 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)')
- ->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'The version of the completion script')
+ ->addOption('api-version', 'a', InputOption::VALUE_REQUIRED, 'The API version of the completion script')
+ ->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'deprecated')
;
}
- protected function initialize(InputInterface $input, OutputInterface $output)
+ protected function initialize(InputInterface $input, OutputInterface $output): void
{
- $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOLEAN);
+ $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOL);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
try {
- // uncomment when a bugfix or BC break has been introduced in the shell completion scripts
- // $version = $input->getOption('symfony');
- // if ($version && version_compare($version, 'x.y', '>=')) {
- // $message = sprintf('Completion script version is not supported ("%s" given, ">=x.y" required).', $version);
- // $this->log($message);
+ // "symfony" must be kept for compat with the shell scripts generated by Symfony Console 5.4 - 6.1
+ $version = $input->getOption('symfony') ? '1' : $input->getOption('api-version');
+ if ($version && version_compare($version, self::COMPLETION_API_VERSION, '<')) {
+ $message = \sprintf('Completion script version is not supported ("%s" given, ">=%s" required).', $version, self::COMPLETION_API_VERSION);
+ $this->log($message);
- // $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.');
+ $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.');
- // return 126;
- // }
+ return 126;
+ }
$shell = $input->getOption('shell');
if (!$shell) {
@@ -81,7 +88,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
if (!$completionOutput = $this->completionOutputs[$shell] ?? false) {
- throw new \RuntimeException(sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode('", "', array_keys($this->completionOutputs))));
+ throw new \RuntimeException(\sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode('", "', array_keys($this->completionOutputs))));
}
$completionInput = $this->createCompletionInput($input);
@@ -91,13 +98,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int
'',
''.date('Y-m-d H:i:s').'>',
'Input:> ("|" indicates the cursor position)>',
- ' '.(string) $completionInput,
+ ' '.$completionInput,
'Command:>',
- ' '.(string) implode(' ', $_SERVER['argv']),
+ ' '.implode(' ', $_SERVER['argv']),
'Messages:>',
]);
- $command = $this->findCommand($completionInput, $output);
+ $command = $this->findCommand($completionInput);
if (null === $command) {
$this->log(' No command found, completing using the Application class.');
@@ -116,12 +123,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$completionInput->bind($command->getDefinition());
if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) {
- $this->log(' Completing option names for the '.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).'> command.');
+ $this->log(' Completing option names for the '.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.'> command.');
$suggestions->suggestOptions($command->getDefinition()->getOptions());
} else {
$this->log([
- ' Completing using the '.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).'> class.',
+ ' Completing using the '.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.'> class.',
' Completing '.$completionInput->getCompletionType().'> for '.$completionInput->getCompletionName().'>',
]);
if (null !== $compval = $completionInput->getCompletionValue()) {
@@ -137,7 +144,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->log('Suggestions:>');
if ($options = $suggestions->getOptionSuggestions()) {
- $this->log(' --'.implode(' --', array_map(function ($o) { return $o->getName(); }, $options)));
+ $this->log(' --'.implode(' --', array_map(fn ($o) => $o->getName(), $options)));
} elseif ($values = $suggestions->getValueSuggestions()) {
$this->log(' '.implode(' ', $values));
} else {
@@ -172,13 +179,13 @@ private function createCompletionInput(InputInterface $input): CompletionInput
try {
$completionInput->bind($this->getApplication()->getDefinition());
- } catch (ExceptionInterface $e) {
+ } catch (ExceptionInterface) {
}
return $completionInput;
}
- private function findCommand(CompletionInput $completionInput, OutputInterface $output): ?Command
+ private function findCommand(CompletionInput $completionInput): ?Command
{
try {
$inputName = $completionInput->getFirstArgument();
@@ -187,7 +194,7 @@ private function findCommand(CompletionInput $completionInput, OutputInterface $
}
return $this->getApplication()->find($inputName);
- } catch (CommandNotFoundException $e) {
+ } catch (CommandNotFoundException) {
}
return null;
diff --git a/Command/DumpCompletionCommand.php b/Command/DumpCompletionCommand.php
index eaf22be1a..2853fc5f4 100644
--- a/Command/DumpCompletionCommand.php
+++ b/Command/DumpCompletionCommand.php
@@ -11,8 +11,7 @@
namespace Symfony\Component\Console\Command;
-use Symfony\Component\Console\Completion\CompletionInput;
-use Symfony\Component\Console\Completion\CompletionSuggestions;
+use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -25,55 +24,57 @@
*
* @author Wouter de Jong
*/
+#[AsCommand(name: 'completion', description: 'Dump the shell completion script')]
final class DumpCompletionCommand extends Command
{
- protected static $defaultName = 'completion';
- protected static $defaultDescription = 'Dump the shell completion script';
+ private array $supportedShells;
- public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
- {
- if ($input->mustSuggestArgumentValuesFor('shell')) {
- $suggestions->suggestValues($this->getSupportedShells());
- }
- }
-
- protected function configure()
+ protected function configure(): void
{
$fullCommand = $_SERVER['PHP_SELF'];
$commandName = basename($fullCommand);
$fullCommand = @realpath($fullCommand) ?: $fullCommand;
+ $shell = self::guessShell();
+ [$rcFile, $completionFile] = match ($shell) {
+ 'fish' => ['~/.config/fish/config.fish', "/etc/fish/completions/$commandName.fish"],
+ 'zsh' => ['~/.zshrc', '$fpath[1]/_'.$commandName],
+ default => ['~/.bashrc', "/etc/bash_completion.d/$commandName"],
+ };
+
+ $supportedShells = implode(', ', $this->getSupportedShells());
+
$this
->setHelp(<<%command.name%> command dumps the shell completion script required
-to use shell autocompletion (currently only bash completion is supported).
+to use shell autocompletion (currently, {$supportedShells} completion are supported).
Static installation
------------------->
Dump the script to a global completion file and restart your shell:
- %command.full_name% bash | sudo tee /etc/bash_completion.d/{$commandName}>
+ %command.full_name% {$shell} | sudo tee {$completionFile}>
Or dump the script to a local file and source it:
- %command.full_name% bash > completion.sh>
+ %command.full_name% {$shell} > completion.sh>
# source the file whenever you use the project>
source completion.sh>
- # or add this line at the end of your "~/.bashrc" file:>
+ # or add this line at the end of your "{$rcFile}" file:>
source /path/to/completion.sh>
Dynamic installation
-------------------->
-Add this to the end of your shell configuration file (e.g. "~/.bashrc">):
+Add this to the end of your shell configuration file (e.g. "{$rcFile}">):
- eval "$({$fullCommand} completion bash)">
+ eval "$({$fullCommand} completion {$shell})">
EOH
)
- ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given')
+ ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given', null, $this->getSupportedShells(...))
->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log')
;
}
@@ -97,15 +98,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$output = $output->getErrorOutput();
}
if ($shell) {
- $output->writeln(sprintf('Detected shell "%s", which is not supported by Symfony shell completion (supported shells: "%s").>', $shell, implode('", "', $supportedShells)));
+ $output->writeln(\sprintf('Detected shell "%s", which is not supported by Symfony shell completion (supported shells: "%s").>', $shell, implode('", "', $supportedShells)));
} else {
- $output->writeln(sprintf('Shell not detected, Symfony shell completion only supports "%s").>', implode('", "', $supportedShells)));
+ $output->writeln(\sprintf('Shell not detected, Symfony shell completion only supports "%s").>', implode('", "', $supportedShells)));
}
return 2;
}
- $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, $this->getApplication()->getVersion()], file_get_contents($completionFile)));
+ $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, CompleteCommand::COMPLETION_API_VERSION], file_get_contents($completionFile)));
return 0;
}
@@ -132,6 +133,10 @@ private function tailDebugLog(string $commandName, OutputInterface $output): voi
*/
private function getSupportedShells(): array
{
+ if (isset($this->supportedShells)) {
+ return $this->supportedShells;
+ }
+
$shells = [];
foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) {
@@ -139,7 +144,8 @@ private function getSupportedShells(): array
$shells[] = $file->getExtension();
}
}
+ sort($shells);
- return $shells;
+ return $this->supportedShells = $shells;
}
}
diff --git a/Command/HelpCommand.php b/Command/HelpCommand.php
index c66ef463e..a2a72dab4 100644
--- a/Command/HelpCommand.php
+++ b/Command/HelpCommand.php
@@ -11,8 +11,6 @@
namespace Symfony\Component\Console\Command;
-use Symfony\Component\Console\Completion\CompletionInput;
-use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Descriptor\ApplicationDescription;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputArgument;
@@ -27,20 +25,17 @@
*/
class HelpCommand extends Command
{
- private $command;
+ private Command $command;
- /**
- * {@inheritdoc}
- */
- protected function configure()
+ protected function configure(): void
{
$this->ignoreValidationErrors();
$this
->setName('help')
->setDefinition([
- new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'),
- new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'),
+ new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help', fn () => array_keys((new ApplicationDescription($this->getApplication()))->getCommands())),
+ new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()),
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'),
])
->setDescription('Display help for a command')
@@ -59,19 +54,14 @@ protected function configure()
;
}
- public function setCommand(Command $command)
+ public function setCommand(Command $command): void
{
$this->command = $command;
}
- /**
- * {@inheritdoc}
- */
- protected function execute(InputInterface $input, OutputInterface $output)
+ protected function execute(InputInterface $input, OutputInterface $output): int
{
- if (null === $this->command) {
- $this->command = $this->getApplication()->find($input->getArgument('command_name'));
- }
+ $this->command ??= $this->getApplication()->find($input->getArgument('command_name'));
$helper = new DescriptorHelper();
$helper->describe($output, $this->command, [
@@ -79,23 +69,8 @@ protected function execute(InputInterface $input, OutputInterface $output)
'raw_text' => $input->getOption('raw'),
]);
- $this->command = null;
+ unset($this->command);
return 0;
}
-
- public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
- {
- if ($input->mustSuggestArgumentValuesFor('command_name')) {
- $descriptor = new ApplicationDescription($this->getApplication());
- $suggestions->suggestValues(array_keys($descriptor->getCommands()));
-
- return;
- }
-
- if ($input->mustSuggestOptionValuesFor('format')) {
- $helper = new DescriptorHelper();
- $suggestions->suggestValues($helper->getFormats());
- }
- }
}
diff --git a/Command/LazyCommand.php b/Command/LazyCommand.php
index 302a0809e..fd2c300d7 100644
--- a/Command/LazyCommand.php
+++ b/Command/LazyCommand.php
@@ -14,6 +14,8 @@
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
+use Symfony\Component\Console\Completion\Suggestion;
+use Symfony\Component\Console\Helper\HelperInterface;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
@@ -24,18 +26,22 @@
*/
final class LazyCommand extends Command
{
- private $command;
- private $isEnabled;
-
- public function __construct(string $name, array $aliases, string $description, bool $isHidden, \Closure $commandFactory, ?bool $isEnabled = true)
- {
+ private \Closure|Command $command;
+
+ public function __construct(
+ string $name,
+ array $aliases,
+ string $description,
+ bool $isHidden,
+ \Closure $commandFactory,
+ private ?bool $isEnabled = true,
+ ) {
$this->setName($name)
->setAliases($aliases)
->setHidden($isHidden)
->setDescription($description);
$this->command = $commandFactory;
- $this->isEnabled = $isEnabled;
}
public function ignoreValidationErrors(): void
@@ -43,7 +49,7 @@ public function ignoreValidationErrors(): void
$this->getCommand()->ignoreValidationErrors();
}
- public function setApplication(?Application $application = null): void
+ public function setApplication(?Application $application): void
{
if ($this->command instanceof parent) {
$this->command->setApplication($application);
@@ -76,10 +82,7 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti
$this->getCommand()->complete($input, $suggestions);
}
- /**
- * @return $this
- */
- public function setCode(callable $code): self
+ public function setCode(callable $code): static
{
$this->getCommand()->setCode($code);
@@ -94,10 +97,7 @@ public function mergeApplicationDefinition(bool $mergeArgs = true): void
$this->getCommand()->mergeApplicationDefinition($mergeArgs);
}
- /**
- * @return $this
- */
- public function setDefinition($definition): self
+ public function setDefinition(array|InputDefinition $definition): static
{
$this->getCommand()->setDefinition($definition);
@@ -115,39 +115,33 @@ public function getNativeDefinition(): InputDefinition
}
/**
- * @return $this
+ * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion
*/
- public function addArgument(string $name, ?int $mode = null, string $description = '', $default = null): self
+ public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
{
- $this->getCommand()->addArgument($name, $mode, $description, $default);
+ $this->getCommand()->addArgument($name, $mode, $description, $default, $suggestedValues);
return $this;
}
/**
- * @return $this
+ * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion
*/
- public function addOption(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null): self
+ public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
{
- $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default);
+ $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);
return $this;
}
- /**
- * @return $this
- */
- public function setProcessTitle(string $title): self
+ public function setProcessTitle(string $title): static
{
$this->getCommand()->setProcessTitle($title);
return $this;
}
- /**
- * @return $this
- */
- public function setHelp(string $help): self
+ public function setHelp(string $help): static
{
$this->getCommand()->setHelp($help);
@@ -169,10 +163,7 @@ public function getSynopsis(bool $short = false): string
return $this->getCommand()->getSynopsis($short);
}
- /**
- * @return $this
- */
- public function addUsage(string $usage): self
+ public function addUsage(string $usage): static
{
$this->getCommand()->addUsage($usage);
@@ -184,10 +175,7 @@ public function getUsages(): array
return $this->getCommand()->getUsages();
}
- /**
- * @return mixed
- */
- public function getHelper(string $name)
+ public function getHelper(string $name): HelperInterface
{
return $this->getCommand()->getHelper($name);
}
diff --git a/Command/ListCommand.php b/Command/ListCommand.php
index f04a4ef67..61b4b1b3e 100644
--- a/Command/ListCommand.php
+++ b/Command/ListCommand.php
@@ -11,8 +11,6 @@
namespace Symfony\Component\Console\Command;
-use Symfony\Component\Console\Completion\CompletionInput;
-use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Descriptor\ApplicationDescription;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputArgument;
@@ -27,17 +25,14 @@
*/
class ListCommand extends Command
{
- /**
- * {@inheritdoc}
- */
- protected function configure()
+ protected function configure(): void
{
$this
->setName('list')
->setDefinition([
- new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'),
+ new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, fn () => array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces())),
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
- new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'),
+ new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()),
new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'),
])
->setDescription('List commands')
@@ -62,10 +57,7 @@ protected function configure()
;
}
- /**
- * {@inheritdoc}
- */
- protected function execute(InputInterface $input, OutputInterface $output)
+ protected function execute(InputInterface $input, OutputInterface $output): int
{
$helper = new DescriptorHelper();
$helper->describe($output, $this->getApplication(), [
@@ -77,19 +69,4 @@ protected function execute(InputInterface $input, OutputInterface $output)
return 0;
}
-
- public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
- {
- if ($input->mustSuggestArgumentValuesFor('namespace')) {
- $descriptor = new ApplicationDescription($this->getApplication());
- $suggestions->suggestValues(array_keys($descriptor->getNamespaces()));
-
- return;
- }
-
- if ($input->mustSuggestOptionValuesFor('format')) {
- $helper = new DescriptorHelper();
- $suggestions->suggestValues($helper->getFormats());
- }
- }
}
diff --git a/Command/LockableTrait.php b/Command/LockableTrait.php
index d21edc2c0..f0001cc52 100644
--- a/Command/LockableTrait.php
+++ b/Command/LockableTrait.php
@@ -24,8 +24,9 @@
*/
trait LockableTrait
{
- /** @var LockInterface|null */
- private $lock;
+ private ?LockInterface $lock = null;
+
+ private ?LockFactory $lockFactory = null;
/**
* Locks a command.
@@ -33,20 +34,24 @@ trait LockableTrait
private function lock(?string $name = null, bool $blocking = false): bool
{
if (!class_exists(SemaphoreStore::class)) {
- throw new LogicException('To enable the locking feature you must install the symfony/lock component.');
+ throw new LogicException('To enable the locking feature you must install the symfony/lock component. Try running "composer require symfony/lock".');
}
if (null !== $this->lock) {
throw new LogicException('A lock is already in place.');
}
- if (SemaphoreStore::isSupported()) {
- $store = new SemaphoreStore();
- } else {
- $store = new FlockStore();
+ if (null === $this->lockFactory) {
+ if (SemaphoreStore::isSupported()) {
+ $store = new SemaphoreStore();
+ } else {
+ $store = new FlockStore();
+ }
+
+ $this->lockFactory = (new LockFactory($store));
}
- $this->lock = (new LockFactory($store))->createLock($name ?: $this->getName());
+ $this->lock = $this->lockFactory->createLock($name ?: $this->getName());
if (!$this->lock->acquire($blocking)) {
$this->lock = null;
@@ -59,7 +64,7 @@ private function lock(?string $name = null, bool $blocking = false): bool
/**
* Releases the command lock if there is one.
*/
- private function release()
+ private function release(): void
{
if ($this->lock) {
$this->lock->release();
diff --git a/Command/SignalableCommandInterface.php b/Command/SignalableCommandInterface.php
index d439728b6..40b301d18 100644
--- a/Command/SignalableCommandInterface.php
+++ b/Command/SignalableCommandInterface.php
@@ -25,6 +25,8 @@ public function getSubscribedSignals(): array;
/**
* The method will be called when the application is signaled.
+ *
+ * @return int|false The exit code to return or false to continue the normal execution
*/
- public function handleSignal(int $signal): void;
+ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false;
}
diff --git a/Command/TraceableCommand.php b/Command/TraceableCommand.php
new file mode 100644
index 000000000..9ffb68da3
--- /dev/null
+++ b/Command/TraceableCommand.php
@@ -0,0 +1,356 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Command;
+
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Completion\CompletionInput;
+use Symfony\Component\Console\Completion\CompletionSuggestions;
+use Symfony\Component\Console\Helper\HelperInterface;
+use Symfony\Component\Console\Helper\HelperSet;
+use Symfony\Component\Console\Input\InputDefinition;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\ConsoleOutputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Stopwatch\Stopwatch;
+
+/**
+ * @internal
+ *
+ * @author Jules Pietri
+ */
+final class TraceableCommand extends Command implements SignalableCommandInterface
+{
+ public readonly Command $command;
+ public int $exitCode;
+ public ?int $interruptedBySignal = null;
+ public bool $ignoreValidation;
+ public bool $isInteractive = false;
+ public string $duration = 'n/a';
+ public string $maxMemoryUsage = 'n/a';
+ public InputInterface $input;
+ public OutputInterface $output;
+ /** @var array */
+ public array $arguments;
+ /** @var array */
+ public array $options;
+ /** @var array */
+ public array $interactiveInputs = [];
+ public array $handledSignals = [];
+
+ public function __construct(
+ Command $command,
+ private readonly Stopwatch $stopwatch,
+ ) {
+ if ($command instanceof LazyCommand) {
+ $command = $command->getCommand();
+ }
+
+ $this->command = $command;
+
+ // prevent call to self::getDefaultDescription()
+ $this->setDescription($command->getDescription());
+
+ parent::__construct($command->getName());
+
+ // init below enables calling {@see parent::run()}
+ [$code, $processTitle, $ignoreValidationErrors] = \Closure::bind(function () {
+ return [$this->code, $this->processTitle, $this->ignoreValidationErrors];
+ }, $command, Command::class)();
+
+ if (\is_callable($code)) {
+ $this->setCode($code);
+ }
+
+ if ($processTitle) {
+ parent::setProcessTitle($processTitle);
+ }
+
+ if ($ignoreValidationErrors) {
+ parent::ignoreValidationErrors();
+ }
+
+ $this->ignoreValidation = $ignoreValidationErrors;
+ }
+
+ public function __call(string $name, array $arguments): mixed
+ {
+ return $this->command->{$name}(...$arguments);
+ }
+
+ public function getSubscribedSignals(): array
+ {
+ return $this->command instanceof SignalableCommandInterface ? $this->command->getSubscribedSignals() : [];
+ }
+
+ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
+ {
+ if (!$this->command instanceof SignalableCommandInterface) {
+ return false;
+ }
+
+ $event = $this->stopwatch->start($this->getName().'.handle_signal');
+
+ $exit = $this->command->handleSignal($signal, $previousExitCode);
+
+ $event->stop();
+
+ if (!isset($this->handledSignals[$signal])) {
+ $this->handledSignals[$signal] = [
+ 'handled' => 0,
+ 'duration' => 0,
+ 'memory' => 0,
+ ];
+ }
+
+ ++$this->handledSignals[$signal]['handled'];
+ $this->handledSignals[$signal]['duration'] += $event->getDuration();
+ $this->handledSignals[$signal]['memory'] = max(
+ $this->handledSignals[$signal]['memory'],
+ $event->getMemory() >> 20
+ );
+
+ return $exit;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Calling parent method is required to be used in {@see parent::run()}.
+ */
+ public function ignoreValidationErrors(): void
+ {
+ $this->ignoreValidation = true;
+ $this->command->ignoreValidationErrors();
+
+ parent::ignoreValidationErrors();
+ }
+
+ public function setApplication(?Application $application = null): void
+ {
+ $this->command->setApplication($application);
+ }
+
+ public function getApplication(): ?Application
+ {
+ return $this->command->getApplication();
+ }
+
+ public function setHelperSet(HelperSet $helperSet): void
+ {
+ $this->command->setHelperSet($helperSet);
+ }
+
+ public function getHelperSet(): ?HelperSet
+ {
+ return $this->command->getHelperSet();
+ }
+
+ public function isEnabled(): bool
+ {
+ return $this->command->isEnabled();
+ }
+
+ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
+ {
+ $this->command->complete($input, $suggestions);
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Calling parent method is required to be used in {@see parent::run()}.
+ */
+ public function setCode(callable $code): static
+ {
+ $this->command->setCode($code);
+
+ return parent::setCode(function (InputInterface $input, OutputInterface $output) use ($code): int {
+ $event = $this->stopwatch->start($this->getName().'.code');
+
+ $this->exitCode = $code($input, $output);
+
+ $event->stop();
+
+ return $this->exitCode;
+ });
+ }
+
+ /**
+ * @internal
+ */
+ public function mergeApplicationDefinition(bool $mergeArgs = true): void
+ {
+ $this->command->mergeApplicationDefinition($mergeArgs);
+ }
+
+ public function setDefinition(array|InputDefinition $definition): static
+ {
+ $this->command->setDefinition($definition);
+
+ return $this;
+ }
+
+ public function getDefinition(): InputDefinition
+ {
+ return $this->command->getDefinition();
+ }
+
+ public function getNativeDefinition(): InputDefinition
+ {
+ return $this->command->getNativeDefinition();
+ }
+
+ public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
+ {
+ $this->command->addArgument($name, $mode, $description, $default, $suggestedValues);
+
+ return $this;
+ }
+
+ public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
+ {
+ $this->command->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Calling parent method is required to be used in {@see parent::run()}.
+ */
+ public function setProcessTitle(string $title): static
+ {
+ $this->command->setProcessTitle($title);
+
+ return parent::setProcessTitle($title);
+ }
+
+ public function setHelp(string $help): static
+ {
+ $this->command->setHelp($help);
+
+ return $this;
+ }
+
+ public function getHelp(): string
+ {
+ return $this->command->getHelp();
+ }
+
+ public function getProcessedHelp(): string
+ {
+ return $this->command->getProcessedHelp();
+ }
+
+ public function getSynopsis(bool $short = false): string
+ {
+ return $this->command->getSynopsis($short);
+ }
+
+ public function addUsage(string $usage): static
+ {
+ $this->command->addUsage($usage);
+
+ return $this;
+ }
+
+ public function getUsages(): array
+ {
+ return $this->command->getUsages();
+ }
+
+ public function getHelper(string $name): HelperInterface
+ {
+ return $this->command->getHelper($name);
+ }
+
+ public function run(InputInterface $input, OutputInterface $output): int
+ {
+ $this->input = $input;
+ $this->output = $output;
+ $this->arguments = $input->getArguments();
+ $this->options = $input->getOptions();
+ $event = $this->stopwatch->start($this->getName(), 'command');
+
+ try {
+ $this->exitCode = parent::run($input, $output);
+ } finally {
+ $event->stop();
+
+ if ($output instanceof ConsoleOutputInterface && $output->isDebug()) {
+ $output->getErrorOutput()->writeln((string) $event);
+ }
+
+ $this->duration = $event->getDuration().' ms';
+ $this->maxMemoryUsage = ($event->getMemory() >> 20).' MiB';
+
+ if ($this->isInteractive) {
+ $this->extractInteractiveInputs($input->getArguments(), $input->getOptions());
+ }
+ }
+
+ return $this->exitCode;
+ }
+
+ protected function initialize(InputInterface $input, OutputInterface $output): void
+ {
+ $event = $this->stopwatch->start($this->getName().'.init', 'command');
+
+ $this->command->initialize($input, $output);
+
+ $event->stop();
+ }
+
+ protected function interact(InputInterface $input, OutputInterface $output): void
+ {
+ if (!$this->isInteractive = Command::class !== (new \ReflectionMethod($this->command, 'interact'))->getDeclaringClass()->getName()) {
+ return;
+ }
+
+ $event = $this->stopwatch->start($this->getName().'.interact', 'command');
+
+ $this->command->interact($input, $output);
+
+ $event->stop();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $event = $this->stopwatch->start($this->getName().'.execute', 'command');
+
+ $exitCode = $this->command->execute($input, $output);
+
+ $event->stop();
+
+ return $exitCode;
+ }
+
+ private function extractInteractiveInputs(array $arguments, array $options): void
+ {
+ foreach ($arguments as $argName => $argValue) {
+ if (\array_key_exists($argName, $this->arguments) && $this->arguments[$argName] === $argValue) {
+ continue;
+ }
+
+ $this->interactiveInputs[$argName] = $argValue;
+ }
+
+ foreach ($options as $optName => $optValue) {
+ if (\array_key_exists($optName, $this->options) && $this->options[$optName] === $optValue) {
+ continue;
+ }
+
+ $this->interactiveInputs['--'.$optName] = $optValue;
+ }
+ }
+}
diff --git a/CommandLoader/CommandLoaderInterface.php b/CommandLoader/CommandLoaderInterface.php
index 0adaf886f..b6b637ce6 100644
--- a/CommandLoader/CommandLoaderInterface.php
+++ b/CommandLoader/CommandLoaderInterface.php
@@ -22,21 +22,17 @@ interface CommandLoaderInterface
/**
* Loads a command.
*
- * @return Command
- *
* @throws CommandNotFoundException
*/
- public function get(string $name);
+ public function get(string $name): Command;
/**
* Checks if a command exists.
- *
- * @return bool
*/
- public function has(string $name);
+ public function has(string $name): bool;
/**
* @return string[]
*/
- public function getNames();
+ public function getNames(): array;
}
diff --git a/CommandLoader/ContainerCommandLoader.php b/CommandLoader/ContainerCommandLoader.php
index ddccb3d45..eb4945135 100644
--- a/CommandLoader/ContainerCommandLoader.php
+++ b/CommandLoader/ContainerCommandLoader.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Console\CommandLoader;
use Psr\Container\ContainerInterface;
+use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
@@ -21,42 +22,30 @@
*/
class ContainerCommandLoader implements CommandLoaderInterface
{
- private $container;
- private $commandMap;
-
/**
* @param array $commandMap An array with command names as keys and service ids as values
*/
- public function __construct(ContainerInterface $container, array $commandMap)
- {
- $this->container = $container;
- $this->commandMap = $commandMap;
+ public function __construct(
+ private ContainerInterface $container,
+ private array $commandMap,
+ ) {
}
- /**
- * {@inheritdoc}
- */
- public function get(string $name)
+ public function get(string $name): Command
{
if (!$this->has($name)) {
- throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
+ throw new CommandNotFoundException(\sprintf('Command "%s" does not exist.', $name));
}
return $this->container->get($this->commandMap[$name]);
}
- /**
- * {@inheritdoc}
- */
- public function has(string $name)
+ public function has(string $name): bool
{
return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]);
}
- /**
- * {@inheritdoc}
- */
- public function getNames()
+ public function getNames(): array
{
return array_keys($this->commandMap);
}
diff --git a/CommandLoader/FactoryCommandLoader.php b/CommandLoader/FactoryCommandLoader.php
index 7e2db3464..2d13139c2 100644
--- a/CommandLoader/FactoryCommandLoader.php
+++ b/CommandLoader/FactoryCommandLoader.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Console\CommandLoader;
+use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\CommandNotFoundException;
/**
@@ -20,31 +21,23 @@
*/
class FactoryCommandLoader implements CommandLoaderInterface
{
- private $factories;
-
/**
* @param callable[] $factories Indexed by command names
*/
- public function __construct(array $factories)
- {
- $this->factories = $factories;
+ public function __construct(
+ private array $factories,
+ ) {
}
- /**
- * {@inheritdoc}
- */
- public function has(string $name)
+ public function has(string $name): bool
{
return isset($this->factories[$name]);
}
- /**
- * {@inheritdoc}
- */
- public function get(string $name)
+ public function get(string $name): Command
{
if (!isset($this->factories[$name])) {
- throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
+ throw new CommandNotFoundException(\sprintf('Command "%s" does not exist.', $name));
}
$factory = $this->factories[$name];
@@ -52,10 +45,7 @@ public function get(string $name)
return $factory();
}
- /**
- * {@inheritdoc}
- */
- public function getNames()
+ public function getNames(): array
{
return array_keys($this->factories);
}
diff --git a/Completion/CompletionInput.php b/Completion/CompletionInput.php
index 2f631bcd8..9f9619e18 100644
--- a/Completion/CompletionInput.php
+++ b/Completion/CompletionInput.php
@@ -31,11 +31,11 @@ final class CompletionInput extends ArgvInput
public const TYPE_OPTION_NAME = 'option_name';
public const TYPE_NONE = 'none';
- private $tokens;
- private $currentIndex;
- private $completionType;
- private $completionName = null;
- private $completionValue = '';
+ private array $tokens;
+ private int $currentIndex;
+ private string $completionType;
+ private ?string $completionName = null;
+ private string $completionValue = '';
/**
* Converts a terminal string into tokens.
@@ -64,9 +64,6 @@ public static function fromTokens(array $tokens, int $currentIndex): self
return $input;
}
- /**
- * {@inheritdoc}
- */
public function bind(InputDefinition $definition): void
{
parent::bind($definition);
@@ -84,7 +81,7 @@ public function bind(InputDefinition $definition): void
return;
}
- if (null !== $option && $option->acceptValue()) {
+ if ($option?->acceptValue()) {
$this->completionType = self::TYPE_OPTION_VALUE;
$this->completionName = $option->getName();
$this->completionValue = $optionValue ?: (!str_starts_with($optionToken, '--') ? substr($optionToken, 2) : '');
@@ -97,7 +94,7 @@ public function bind(InputDefinition $definition): void
if ('-' === $previousToken[0] && '' !== trim($previousToken, '-')) {
// check if previous option accepted a value
$previousOption = $this->getOptionFromToken($previousToken);
- if (null !== $previousOption && $previousOption->acceptValue()) {
+ if ($previousOption?->acceptValue()) {
$this->completionType = self::TYPE_OPTION_VALUE;
$this->completionName = $previousOption->getName();
$this->completionValue = $relevantToken;
@@ -126,13 +123,13 @@ public function bind(InputDefinition $definition): void
if ($this->currentIndex >= \count($this->tokens)) {
if (!isset($this->arguments[$argumentName]) || $this->definition->getArgument($argumentName)->isArray()) {
$this->completionName = $argumentName;
- $this->completionValue = '';
} else {
// we've reached the end
$this->completionType = self::TYPE_NONE;
$this->completionName = null;
- $this->completionValue = '';
}
+
+ $this->completionValue = '';
}
}
@@ -144,7 +141,9 @@ public function bind(InputDefinition $definition): void
* TYPE_OPTION_NAME when completing the name of an input option
* TYPE_NONE when nothing should be completed
*
- * @return string One of self::TYPE_* constants. TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component
+ * TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component.
+ *
+ * @return self::TYPE_*
*/
public function getCompletionType(): string
{
@@ -183,7 +182,7 @@ protected function parseToken(string $token, bool $parseOptions): bool
{
try {
return parent::parseToken($token, $parseOptions);
- } catch (RuntimeException $e) {
+ } catch (RuntimeException) {
// suppress errors, completed input is almost never valid
}
@@ -227,7 +226,7 @@ private function isCursorFree(): bool
return $this->currentIndex >= $nrOfTokens;
}
- public function __toString()
+ public function __toString(): string
{
$str = '';
foreach ($this->tokens as $i => $token) {
diff --git a/Completion/CompletionSuggestions.php b/Completion/CompletionSuggestions.php
index d8905e5ee..549bbafbd 100644
--- a/Completion/CompletionSuggestions.php
+++ b/Completion/CompletionSuggestions.php
@@ -20,17 +20,15 @@
*/
final class CompletionSuggestions
{
- private $valueSuggestions = [];
- private $optionSuggestions = [];
+ private array $valueSuggestions = [];
+ private array $optionSuggestions = [];
/**
* Add a suggested value for an input option or argument.
*
- * @param string|Suggestion $value
- *
* @return $this
*/
- public function suggestValue($value): self
+ public function suggestValue(string|Suggestion $value): static
{
$this->valueSuggestions[] = !$value instanceof Suggestion ? new Suggestion($value) : $value;
@@ -44,7 +42,7 @@ public function suggestValue($value): self
*
* @return $this
*/
- public function suggestValues(array $values): self
+ public function suggestValues(array $values): static
{
foreach ($values as $value) {
$this->suggestValue($value);
@@ -58,7 +56,7 @@ public function suggestValues(array $values): self
*
* @return $this
*/
- public function suggestOption(InputOption $option): self
+ public function suggestOption(InputOption $option): static
{
$this->optionSuggestions[] = $option;
@@ -72,7 +70,7 @@ public function suggestOption(InputOption $option): self
*
* @return $this
*/
- public function suggestOptions(array $options): self
+ public function suggestOptions(array $options): static
{
foreach ($options as $option) {
$this->suggestOption($option);
diff --git a/Completion/Output/FishCompletionOutput.php b/Completion/Output/FishCompletionOutput.php
new file mode 100644
index 000000000..356a974ea
--- /dev/null
+++ b/Completion/Output/FishCompletionOutput.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Completion\Output;
+
+use Symfony\Component\Console\Completion\CompletionSuggestions;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * @author Guillaume Aveline
+ */
+class FishCompletionOutput implements CompletionOutputInterface
+{
+ public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
+ {
+ $values = [];
+ foreach ($suggestions->getValueSuggestions() as $value) {
+ $values[] = $value->getValue().($value->getDescription() ? "\t".$value->getDescription() : '');
+ }
+ foreach ($suggestions->getOptionSuggestions() as $option) {
+ $values[] = '--'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
+ if ($option->isNegatable()) {
+ $values[] = '--no-'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
+ }
+ }
+ $output->write(implode("\n", $values));
+ }
+}
diff --git a/Completion/Output/ZshCompletionOutput.php b/Completion/Output/ZshCompletionOutput.php
new file mode 100644
index 000000000..bb4ce70b5
--- /dev/null
+++ b/Completion/Output/ZshCompletionOutput.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Completion\Output;
+
+use Symfony\Component\Console\Completion\CompletionSuggestions;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * @author Jitendra A
+ */
+class ZshCompletionOutput implements CompletionOutputInterface
+{
+ public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
+ {
+ $values = [];
+ foreach ($suggestions->getValueSuggestions() as $value) {
+ $values[] = $value->getValue().($value->getDescription() ? "\t".$value->getDescription() : '');
+ }
+ foreach ($suggestions->getOptionSuggestions() as $option) {
+ $values[] = '--'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
+ if ($option->isNegatable()) {
+ $values[] = '--no-'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
+ }
+ }
+ $output->write(implode("\n", $values)."\n");
+ }
+}
diff --git a/Completion/Suggestion.php b/Completion/Suggestion.php
index 6c7bc4dc4..3251b079f 100644
--- a/Completion/Suggestion.php
+++ b/Completion/Suggestion.php
@@ -16,13 +16,12 @@
*
* @author Wouter de Jong
*/
-class Suggestion
+class Suggestion implements \Stringable
{
- private $value;
-
- public function __construct(string $value)
- {
- $this->value = $value;
+ public function __construct(
+ private readonly string $value,
+ private readonly string $description = '',
+ ) {
}
public function getValue(): string
@@ -30,6 +29,11 @@ public function getValue(): string
return $this->value;
}
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
public function __toString(): string
{
return $this->getValue();
diff --git a/Cursor.php b/Cursor.php
index 0c4dafb6c..e2618cf1d 100644
--- a/Cursor.php
+++ b/Cursor.php
@@ -18,24 +18,25 @@
*/
final class Cursor
{
- private $output;
+ /** @var resource */
private $input;
/**
* @param resource|null $input
*/
- public function __construct(OutputInterface $output, $input = null)
- {
- $this->output = $output;
+ public function __construct(
+ private OutputInterface $output,
+ $input = null,
+ ) {
$this->input = $input ?? (\defined('STDIN') ? \STDIN : fopen('php://input', 'r+'));
}
/**
* @return $this
*/
- public function moveUp(int $lines = 1): self
+ public function moveUp(int $lines = 1): static
{
- $this->output->write(sprintf("\x1b[%dA", $lines));
+ $this->output->write(\sprintf("\x1b[%dA", $lines));
return $this;
}
@@ -43,9 +44,9 @@ public function moveUp(int $lines = 1): self
/**
* @return $this
*/
- public function moveDown(int $lines = 1): self
+ public function moveDown(int $lines = 1): static
{
- $this->output->write(sprintf("\x1b[%dB", $lines));
+ $this->output->write(\sprintf("\x1b[%dB", $lines));
return $this;
}
@@ -53,9 +54,9 @@ public function moveDown(int $lines = 1): self
/**
* @return $this
*/
- public function moveRight(int $columns = 1): self
+ public function moveRight(int $columns = 1): static
{
- $this->output->write(sprintf("\x1b[%dC", $columns));
+ $this->output->write(\sprintf("\x1b[%dC", $columns));
return $this;
}
@@ -63,9 +64,9 @@ public function moveRight(int $columns = 1): self
/**
* @return $this
*/
- public function moveLeft(int $columns = 1): self
+ public function moveLeft(int $columns = 1): static
{
- $this->output->write(sprintf("\x1b[%dD", $columns));
+ $this->output->write(\sprintf("\x1b[%dD", $columns));
return $this;
}
@@ -73,9 +74,9 @@ public function moveLeft(int $columns = 1): self
/**
* @return $this
*/
- public function moveToColumn(int $column): self
+ public function moveToColumn(int $column): static
{
- $this->output->write(sprintf("\x1b[%dG", $column));
+ $this->output->write(\sprintf("\x1b[%dG", $column));
return $this;
}
@@ -83,9 +84,9 @@ public function moveToColumn(int $column): self
/**
* @return $this
*/
- public function moveToPosition(int $column, int $row): self
+ public function moveToPosition(int $column, int $row): static
{
- $this->output->write(sprintf("\x1b[%d;%dH", $row + 1, $column));
+ $this->output->write(\sprintf("\x1b[%d;%dH", $row + 1, $column));
return $this;
}
@@ -93,7 +94,7 @@ public function moveToPosition(int $column, int $row): self
/**
* @return $this
*/
- public function savePosition(): self
+ public function savePosition(): static
{
$this->output->write("\x1b7");
@@ -103,7 +104,7 @@ public function savePosition(): self
/**
* @return $this
*/
- public function restorePosition(): self
+ public function restorePosition(): static
{
$this->output->write("\x1b8");
@@ -113,7 +114,7 @@ public function restorePosition(): self
/**
* @return $this
*/
- public function hide(): self
+ public function hide(): static
{
$this->output->write("\x1b[?25l");
@@ -123,7 +124,7 @@ public function hide(): self
/**
* @return $this
*/
- public function show(): self
+ public function show(): static
{
$this->output->write("\x1b[?25h\x1b[?0c");
@@ -135,7 +136,7 @@ public function show(): self
*
* @return $this
*/
- public function clearLine(): self
+ public function clearLine(): static
{
$this->output->write("\x1b[2K");
@@ -157,7 +158,7 @@ public function clearLineAfter(): self
*
* @return $this
*/
- public function clearOutput(): self
+ public function clearOutput(): static
{
$this->output->write("\x1b[0J");
@@ -169,7 +170,7 @@ public function clearOutput(): self
*
* @return $this
*/
- public function clearScreen(): self
+ public function clearScreen(): static
{
$this->output->write("\x1b[2J");
@@ -183,11 +184,7 @@ public function getCurrentPosition(): array
{
static $isTtySupported;
- if (null === $isTtySupported && \function_exists('proc_open')) {
- $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes);
- }
-
- if (!$isTtySupported) {
+ if (!$isTtySupported ??= '/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)) {
return [1, 1];
}
@@ -198,7 +195,7 @@ public function getCurrentPosition(): array
$code = trim(fread($this->input, 1024));
- shell_exec(sprintf('stty %s', $sttyMode));
+ shell_exec(\sprintf('stty %s', $sttyMode));
sscanf($code, "\033[%d;%dR", $row, $col);
diff --git a/DataCollector/CommandDataCollector.php b/DataCollector/CommandDataCollector.php
new file mode 100644
index 000000000..3cbe72b59
--- /dev/null
+++ b/DataCollector/CommandDataCollector.php
@@ -0,0 +1,234 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\DataCollector;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Debug\CliRequest;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\SignalRegistry\SignalMap;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\DataCollector\DataCollector;
+use Symfony\Component\VarDumper\Cloner\Data;
+
+/**
+ * @internal
+ *
+ * @author Jules Pietri
+ */
+final class CommandDataCollector extends DataCollector
+{
+ public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
+ {
+ if (!$request instanceof CliRequest) {
+ return;
+ }
+
+ $command = $request->command;
+ $application = $command->getApplication();
+
+ $this->data = [
+ 'command' => $this->cloneVar($command->command),
+ 'exit_code' => $command->exitCode,
+ 'interrupted_by_signal' => $command->interruptedBySignal,
+ 'duration' => $command->duration,
+ 'max_memory_usage' => $command->maxMemoryUsage,
+ 'verbosity_level' => match ($command->output->getVerbosity()) {
+ OutputInterface::VERBOSITY_QUIET => 'quiet',
+ OutputInterface::VERBOSITY_NORMAL => 'normal',
+ OutputInterface::VERBOSITY_VERBOSE => 'verbose',
+ OutputInterface::VERBOSITY_VERY_VERBOSE => 'very verbose',
+ OutputInterface::VERBOSITY_DEBUG => 'debug',
+ },
+ 'interactive' => $command->isInteractive,
+ 'validate_input' => !$command->ignoreValidation,
+ 'enabled' => $command->isEnabled(),
+ 'visible' => !$command->isHidden(),
+ 'input' => $this->cloneVar($command->input),
+ 'output' => $this->cloneVar($command->output),
+ 'interactive_inputs' => array_map($this->cloneVar(...), $command->interactiveInputs),
+ 'signalable' => $command->getSubscribedSignals(),
+ 'handled_signals' => $command->handledSignals,
+ 'helper_set' => array_map($this->cloneVar(...), iterator_to_array($command->getHelperSet())),
+ ];
+
+ $baseDefinition = $application->getDefinition();
+
+ foreach ($command->arguments as $argName => $argValue) {
+ if ($baseDefinition->hasArgument($argName)) {
+ $this->data['application_inputs'][$argName] = $this->cloneVar($argValue);
+ } else {
+ $this->data['arguments'][$argName] = $this->cloneVar($argValue);
+ }
+ }
+
+ foreach ($command->options as $optName => $optValue) {
+ if ($baseDefinition->hasOption($optName)) {
+ $this->data['application_inputs']['--'.$optName] = $this->cloneVar($optValue);
+ } else {
+ $this->data['options'][$optName] = $this->cloneVar($optValue);
+ }
+ }
+ }
+
+ public function getName(): string
+ {
+ return 'command';
+ }
+
+ /**
+ * @return array{
+ * class?: class-string,
+ * executor?: string,
+ * file: string,
+ * line: int,
+ * }
+ */
+ public function getCommand(): array
+ {
+ $class = $this->data['command']->getType();
+ $r = new \ReflectionMethod($class, 'execute');
+
+ if (Command::class !== $r->getDeclaringClass()) {
+ return [
+ 'executor' => $class.'::'.$r->name,
+ 'file' => $r->getFileName(),
+ 'line' => $r->getStartLine(),
+ ];
+ }
+
+ $r = new \ReflectionClass($class);
+
+ return [
+ 'class' => $class,
+ 'file' => $r->getFileName(),
+ 'line' => $r->getStartLine(),
+ ];
+ }
+
+ public function getInterruptedBySignal(): ?string
+ {
+ if (isset($this->data['interrupted_by_signal'])) {
+ return \sprintf('%s (%d)', SignalMap::getSignalName($this->data['interrupted_by_signal']), $this->data['interrupted_by_signal']);
+ }
+
+ return null;
+ }
+
+ public function getDuration(): string
+ {
+ return $this->data['duration'];
+ }
+
+ public function getMaxMemoryUsage(): string
+ {
+ return $this->data['max_memory_usage'];
+ }
+
+ public function getVerbosityLevel(): string
+ {
+ return $this->data['verbosity_level'];
+ }
+
+ public function getInteractive(): bool
+ {
+ return $this->data['interactive'];
+ }
+
+ public function getValidateInput(): bool
+ {
+ return $this->data['validate_input'];
+ }
+
+ public function getEnabled(): bool
+ {
+ return $this->data['enabled'];
+ }
+
+ public function getVisible(): bool
+ {
+ return $this->data['visible'];
+ }
+
+ public function getInput(): Data
+ {
+ return $this->data['input'];
+ }
+
+ public function getOutput(): Data
+ {
+ return $this->data['output'];
+ }
+
+ /**
+ * @return Data[]
+ */
+ public function getArguments(): array
+ {
+ return $this->data['arguments'] ?? [];
+ }
+
+ /**
+ * @return Data[]
+ */
+ public function getOptions(): array
+ {
+ return $this->data['options'] ?? [];
+ }
+
+ /**
+ * @return Data[]
+ */
+ public function getApplicationInputs(): array
+ {
+ return $this->data['application_inputs'] ?? [];
+ }
+
+ /**
+ * @return Data[]
+ */
+ public function getInteractiveInputs(): array
+ {
+ return $this->data['interactive_inputs'] ?? [];
+ }
+
+ public function getSignalable(): array
+ {
+ return array_map(
+ static fn (int $signal): string => \sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal),
+ $this->data['signalable']
+ );
+ }
+
+ public function getHandledSignals(): array
+ {
+ $keys = array_map(
+ static fn (int $signal): string => \sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal),
+ array_keys($this->data['handled_signals'])
+ );
+
+ return array_combine($keys, array_values($this->data['handled_signals']));
+ }
+
+ /**
+ * @return Data[]
+ */
+ public function getHelperSet(): array
+ {
+ return $this->data['helper_set'] ?? [];
+ }
+
+ public function reset(): void
+ {
+ $this->data = [];
+ }
+}
diff --git a/Debug/CliRequest.php b/Debug/CliRequest.php
new file mode 100644
index 000000000..b023db07a
--- /dev/null
+++ b/Debug/CliRequest.php
@@ -0,0 +1,70 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Debug;
+
+use Symfony\Component\Console\Command\TraceableCommand;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * @internal
+ */
+final class CliRequest extends Request
+{
+ public function __construct(
+ public readonly TraceableCommand $command,
+ ) {
+ parent::__construct(
+ attributes: ['_controller' => \get_class($command->command), '_virtual_type' => 'command'],
+ server: $_SERVER,
+ );
+ }
+
+ // Methods below allow to populate a profile, thus enable search and filtering
+ public function getUri(): string
+ {
+ if ($this->server->has('SYMFONY_CLI_BINARY_NAME')) {
+ $binary = $this->server->get('SYMFONY_CLI_BINARY_NAME').' console';
+ } else {
+ $binary = $this->server->get('argv')[0];
+ }
+
+ return $binary.' '.$this->command->input;
+ }
+
+ public function getMethod(): string
+ {
+ return $this->command->isInteractive ? 'INTERACTIVE' : 'BATCH';
+ }
+
+ public function getResponse(): Response
+ {
+ return new class($this->command->exitCode) extends Response {
+ public function __construct(private readonly int $exitCode)
+ {
+ parent::__construct();
+ }
+
+ public function getStatusCode(): int
+ {
+ return $this->exitCode;
+ }
+ };
+ }
+
+ public function getClientIp(): string
+ {
+ $application = $this->command->getApplication();
+
+ return $application->getName().' '.$application->getVersion();
+ }
+}
diff --git a/DependencyInjection/AddConsoleCommandPass.php b/DependencyInjection/AddConsoleCommandPass.php
index 1fbb212e7..f1521602a 100644
--- a/DependencyInjection/AddConsoleCommandPass.php
+++ b/DependencyInjection/AddConsoleCommandPass.php
@@ -29,48 +29,31 @@
*/
class AddConsoleCommandPass implements CompilerPassInterface
{
- private $commandLoaderServiceId;
- private $commandTag;
- private $noPreloadTag;
- private $privateTagName;
-
- public function __construct(string $commandLoaderServiceId = 'console.command_loader', string $commandTag = 'console.command', string $noPreloadTag = 'container.no_preload', string $privateTagName = 'container.private')
- {
- if (0 < \func_num_args()) {
- trigger_deprecation('symfony/console', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
- }
-
- $this->commandLoaderServiceId = $commandLoaderServiceId;
- $this->commandTag = $commandTag;
- $this->noPreloadTag = $noPreloadTag;
- $this->privateTagName = $privateTagName;
- }
-
- public function process(ContainerBuilder $container)
+ public function process(ContainerBuilder $container): void
{
- $commandServices = $container->findTaggedServiceIds($this->commandTag, true);
+ $commandServices = $container->findTaggedServiceIds('console.command', true);
$lazyCommandMap = [];
$lazyCommandRefs = [];
$serviceIds = [];
foreach ($commandServices as $id => $tags) {
$definition = $container->getDefinition($id);
- $definition->addTag($this->noPreloadTag);
+ $definition->addTag('container.no_preload');
$class = $container->getParameterBag()->resolveValue($definition->getClass());
if (isset($tags[0]['command'])) {
$aliases = $tags[0]['command'];
} else {
if (!$r = $container->getReflectionClass($class)) {
- throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
+ throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}
if (!$r->isSubclassOf(Command::class)) {
- throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class));
+ throw new InvalidArgumentException(\sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class));
}
$aliases = str_replace('%', '%%', $class::getDefaultName() ?? '');
}
- $aliases = explode('|', $aliases ?? '');
+ $aliases = explode('|', $aliases);
$commandName = array_shift($aliases);
if ($isHidden = '' === $commandName) {
@@ -78,7 +61,7 @@ public function process(ContainerBuilder $container)
}
if (null === $commandName) {
- if (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag($this->privateTagName)) {
+ if (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag('container.private')) {
$commandId = 'console.command.public_alias.'.$id;
$container->setAlias($commandId, $id)->setPublic(true);
$id = $commandId;
@@ -104,7 +87,7 @@ public function process(ContainerBuilder $container)
$lazyCommandMap[$tag['command']] = $id;
}
- $description = $description ?? $tag['description'] ?? null;
+ $description ??= $tag['description'] ?? null;
}
$definition->addMethodCall('setName', [$commandName]);
@@ -119,10 +102,10 @@ public function process(ContainerBuilder $container)
if (!$description) {
if (!$r = $container->getReflectionClass($class)) {
- throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
+ throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}
if (!$r->isSubclassOf(Command::class)) {
- throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class));
+ throw new InvalidArgumentException(\sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class));
}
$description = str_replace('%', '%%', $class::getDefaultDescription() ?? '');
}
@@ -138,9 +121,9 @@ public function process(ContainerBuilder $container)
}
$container
- ->register($this->commandLoaderServiceId, ContainerCommandLoader::class)
+ ->register('console.command_loader', ContainerCommandLoader::class)
->setPublic(true)
- ->addTag($this->noPreloadTag)
+ ->addTag('container.no_preload')
->setArguments([ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap]);
$container->setParameter('console.command.ids', $serviceIds);
diff --git a/Descriptor/ApplicationDescription.php b/Descriptor/ApplicationDescription.php
index eb11b4f91..802d68560 100644
--- a/Descriptor/ApplicationDescription.php
+++ b/Descriptor/ApplicationDescription.php
@@ -24,35 +24,28 @@ class ApplicationDescription
{
public const GLOBAL_NAMESPACE = '_global';
- private $application;
- private $namespace;
- private $showHidden;
-
- /**
- * @var array
- */
- private $namespaces;
+ private array $namespaces;
/**
* @var array
*/
- private $commands;
+ private array $commands;
/**
* @var array
*/
- private $aliases;
+ private array $aliases = [];
- public function __construct(Application $application, ?string $namespace = null, bool $showHidden = false)
- {
- $this->application = $application;
- $this->namespace = $namespace;
- $this->showHidden = $showHidden;
+ public function __construct(
+ private Application $application,
+ private ?string $namespace = null,
+ private bool $showHidden = false,
+ ) {
}
public function getNamespaces(): array
{
- if (null === $this->namespaces) {
+ if (!isset($this->namespaces)) {
$this->inspectApplication();
}
@@ -64,7 +57,7 @@ public function getNamespaces(): array
*/
public function getCommands(): array
{
- if (null === $this->commands) {
+ if (!isset($this->commands)) {
$this->inspectApplication();
}
@@ -77,13 +70,13 @@ public function getCommands(): array
public function getCommand(string $name): Command
{
if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) {
- throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
+ throw new CommandNotFoundException(\sprintf('Command "%s" does not exist.', $name));
}
return $this->commands[$name] ?? $this->aliases[$name];
}
- private function inspectApplication()
+ private function inspectApplication(): void
{
$this->commands = [];
$this->namespaces = [];
diff --git a/Descriptor/Descriptor.php b/Descriptor/Descriptor.php
index a3648301f..2143a17c3 100644
--- a/Descriptor/Descriptor.php
+++ b/Descriptor/Descriptor.php
@@ -26,43 +26,23 @@
*/
abstract class Descriptor implements DescriptorInterface
{
- /**
- * @var OutputInterface
- */
- protected $output;
+ protected OutputInterface $output;
- /**
- * {@inheritdoc}
- */
- public function describe(OutputInterface $output, object $object, array $options = [])
+ public function describe(OutputInterface $output, object $object, array $options = []): void
{
$this->output = $output;
- switch (true) {
- case $object instanceof InputArgument:
- $this->describeInputArgument($object, $options);
- break;
- case $object instanceof InputOption:
- $this->describeInputOption($object, $options);
- break;
- case $object instanceof InputDefinition:
- $this->describeInputDefinition($object, $options);
- break;
- case $object instanceof Command:
- $this->describeCommand($object, $options);
- break;
- case $object instanceof Application:
- $this->describeApplication($object, $options);
- break;
- default:
- throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object)));
- }
+ match (true) {
+ $object instanceof InputArgument => $this->describeInputArgument($object, $options),
+ $object instanceof InputOption => $this->describeInputOption($object, $options),
+ $object instanceof InputDefinition => $this->describeInputDefinition($object, $options),
+ $object instanceof Command => $this->describeCommand($object, $options),
+ $object instanceof Application => $this->describeApplication($object, $options),
+ default => throw new InvalidArgumentException(\sprintf('Object of type "%s" is not describable.', get_debug_type($object))),
+ };
}
- /**
- * Writes content to output.
- */
- protected function write(string $content, bool $decorated = false)
+ protected function write(string $content, bool $decorated = false): void
{
$this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW);
}
@@ -70,25 +50,25 @@ protected function write(string $content, bool $decorated = false)
/**
* Describes an InputArgument instance.
*/
- abstract protected function describeInputArgument(InputArgument $argument, array $options = []);
+ abstract protected function describeInputArgument(InputArgument $argument, array $options = []): void;
/**
* Describes an InputOption instance.
*/
- abstract protected function describeInputOption(InputOption $option, array $options = []);
+ abstract protected function describeInputOption(InputOption $option, array $options = []): void;
/**
* Describes an InputDefinition instance.
*/
- abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []);
+ abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []): void;
/**
* Describes a Command instance.
*/
- abstract protected function describeCommand(Command $command, array $options = []);
+ abstract protected function describeCommand(Command $command, array $options = []): void;
/**
* Describes an Application instance.
*/
- abstract protected function describeApplication(Application $application, array $options = []);
+ abstract protected function describeApplication(Application $application, array $options = []): void;
}
diff --git a/Descriptor/DescriptorInterface.php b/Descriptor/DescriptorInterface.php
index ebea30367..04e5a7c86 100644
--- a/Descriptor/DescriptorInterface.php
+++ b/Descriptor/DescriptorInterface.php
@@ -20,5 +20,5 @@
*/
interface DescriptorInterface
{
- public function describe(OutputInterface $output, object $object, array $options = []);
+ public function describe(OutputInterface $output, object $object, array $options = []): void;
}
diff --git a/Descriptor/JsonDescriptor.php b/Descriptor/JsonDescriptor.php
index 1d2865941..956303709 100644
--- a/Descriptor/JsonDescriptor.php
+++ b/Descriptor/JsonDescriptor.php
@@ -26,18 +26,12 @@
*/
class JsonDescriptor extends Descriptor
{
- /**
- * {@inheritdoc}
- */
- protected function describeInputArgument(InputArgument $argument, array $options = [])
+ protected function describeInputArgument(InputArgument $argument, array $options = []): void
{
$this->writeData($this->getInputArgumentData($argument), $options);
}
- /**
- * {@inheritdoc}
- */
- protected function describeInputOption(InputOption $option, array $options = [])
+ protected function describeInputOption(InputOption $option, array $options = []): void
{
$this->writeData($this->getInputOptionData($option), $options);
if ($option->isNegatable()) {
@@ -45,26 +39,17 @@ protected function describeInputOption(InputOption $option, array $options = [])
}
}
- /**
- * {@inheritdoc}
- */
- protected function describeInputDefinition(InputDefinition $definition, array $options = [])
+ protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
{
$this->writeData($this->getInputDefinitionData($definition), $options);
}
- /**
- * {@inheritdoc}
- */
- protected function describeCommand(Command $command, array $options = [])
+ protected function describeCommand(Command $command, array $options = []): void
{
$this->writeData($this->getCommandData($command, $options['short'] ?? false), $options);
}
- /**
- * {@inheritdoc}
- */
- protected function describeApplication(Application $application, array $options = [])
+ protected function describeApplication(Application $application, array $options = []): void
{
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace, true);
@@ -96,7 +81,7 @@ protected function describeApplication(Application $application, array $options
/**
* Writes data as json.
*/
- private function writeData(array $data, array $options)
+ private function writeData(array $data, array $options): void
{
$flags = $options['json_encoding'] ?? 0;
diff --git a/Descriptor/MarkdownDescriptor.php b/Descriptor/MarkdownDescriptor.php
index 21ceca6c2..8b7075943 100644
--- a/Descriptor/MarkdownDescriptor.php
+++ b/Descriptor/MarkdownDescriptor.php
@@ -28,10 +28,7 @@
*/
class MarkdownDescriptor extends Descriptor
{
- /**
- * {@inheritdoc}
- */
- public function describe(OutputInterface $output, object $object, array $options = [])
+ public function describe(OutputInterface $output, object $object, array $options = []): void
{
$decorated = $output->isDecorated();
$output->setDecorated(false);
@@ -41,18 +38,12 @@ public function describe(OutputInterface $output, object $object, array $options
$output->setDecorated($decorated);
}
- /**
- * {@inheritdoc}
- */
- protected function write(string $content, bool $decorated = true)
+ protected function write(string $content, bool $decorated = true): void
{
parent::write($content, $decorated);
}
- /**
- * {@inheritdoc}
- */
- protected function describeInputArgument(InputArgument $argument, array $options = [])
+ protected function describeInputArgument(InputArgument $argument, array $options = []): void
{
$this->write(
'#### `'.($argument->getName() ?: '')."`\n\n"
@@ -63,10 +54,7 @@ protected function describeInputArgument(InputArgument $argument, array $options
);
}
- /**
- * {@inheritdoc}
- */
- protected function describeInputOption(InputOption $option, array $options = [])
+ protected function describeInputOption(InputOption $option, array $options = []): void
{
$name = '--'.$option->getName();
if ($option->isNegatable()) {
@@ -87,18 +75,13 @@ protected function describeInputOption(InputOption $option, array $options = [])
);
}
- /**
- * {@inheritdoc}
- */
- protected function describeInputDefinition(InputDefinition $definition, array $options = [])
+ protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
{
if ($showArguments = \count($definition->getArguments()) > 0) {
$this->write('### Arguments');
foreach ($definition->getArguments() as $argument) {
$this->write("\n\n");
- if (null !== $describeInputArgument = $this->describeInputArgument($argument)) {
- $this->write($describeInputArgument);
- }
+ $this->describeInputArgument($argument);
}
}
@@ -110,17 +93,12 @@ protected function describeInputDefinition(InputDefinition $definition, array $o
$this->write('### Options');
foreach ($definition->getOptions() as $option) {
$this->write("\n\n");
- if (null !== $describeInputOption = $this->describeInputOption($option)) {
- $this->write($describeInputOption);
- }
+ $this->describeInputOption($option);
}
}
}
- /**
- * {@inheritdoc}
- */
- protected function describeCommand(Command $command, array $options = [])
+ protected function describeCommand(Command $command, array $options = []): void
{
if ($options['short'] ?? false) {
$this->write(
@@ -128,9 +106,7 @@ protected function describeCommand(Command $command, array $options = [])
.str_repeat('-', Helper::width($command->getName()) + 2)."\n\n"
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
.'### Usage'."\n\n"
- .array_reduce($command->getAliases(), function ($carry, $usage) {
- return $carry.'* `'.$usage.'`'."\n";
- })
+ .array_reduce($command->getAliases(), fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n")
);
return;
@@ -143,9 +119,7 @@ protected function describeCommand(Command $command, array $options = [])
.str_repeat('-', Helper::width($command->getName()) + 2)."\n\n"
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
.'### Usage'."\n\n"
- .array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), function ($carry, $usage) {
- return $carry.'* `'.$usage.'`'."\n";
- })
+ .array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n")
);
if ($help = $command->getProcessedHelp()) {
@@ -160,10 +134,7 @@ protected function describeCommand(Command $command, array $options = [])
}
}
- /**
- * {@inheritdoc}
- */
- protected function describeApplication(Application $application, array $options = [])
+ protected function describeApplication(Application $application, array $options = []): void
{
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace);
@@ -178,16 +149,12 @@ protected function describeApplication(Application $application, array $options
}
$this->write("\n\n");
- $this->write(implode("\n", array_map(function ($commandName) use ($description) {
- return sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName()));
- }, $namespace['commands'])));
+ $this->write(implode("\n", array_map(fn ($commandName) => \sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName())), $namespace['commands'])));
}
foreach ($description->getCommands() as $command) {
$this->write("\n\n");
- if (null !== $describeCommand = $this->describeCommand($command, $options)) {
- $this->write($describeCommand);
- }
+ $this->describeCommand($command, $options);
}
}
@@ -195,7 +162,7 @@ private function getApplicationTitle(Application $application): string
{
if ('UNKNOWN' !== $application->getName()) {
if ('UNKNOWN' !== $application->getVersion()) {
- return sprintf('%s %s', $application->getName(), $application->getVersion());
+ return \sprintf('%s %s', $application->getName(), $application->getVersion());
}
return $application->getName();
diff --git a/Descriptor/ReStructuredTextDescriptor.php b/Descriptor/ReStructuredTextDescriptor.php
new file mode 100644
index 000000000..d2dde6fba
--- /dev/null
+++ b/Descriptor/ReStructuredTextDescriptor.php
@@ -0,0 +1,273 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Descriptor;
+
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Helper\Helper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputDefinition;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\String\UnicodeString;
+
+class ReStructuredTextDescriptor extends Descriptor
+{
+ //
+ private string $partChar = '=';
+ //
+ private string $chapterChar = '-';
+ //
+ private string $sectionChar = '~';
+ //
+ private string $subsectionChar = '.';
+ //
+ private string $subsubsectionChar = '^';
+ //
+ private string $paragraphsChar = '"';
+
+ private array $visibleNamespaces = [];
+
+ public function describe(OutputInterface $output, object $object, array $options = []): void
+ {
+ $decorated = $output->isDecorated();
+ $output->setDecorated(false);
+
+ parent::describe($output, $object, $options);
+
+ $output->setDecorated($decorated);
+ }
+
+ /**
+ * Override parent method to set $decorated = true.
+ */
+ protected function write(string $content, bool $decorated = true): void
+ {
+ parent::write($content, $decorated);
+ }
+
+ protected function describeInputArgument(InputArgument $argument, array $options = []): void
+ {
+ $this->write(
+ $argument->getName() ?: ''."\n".str_repeat($this->paragraphsChar, Helper::width($argument->getName()))."\n\n"
+ .($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '')
+ .'- **Is required**: '.($argument->isRequired() ? 'yes' : 'no')."\n"
+ .'- **Is array**: '.($argument->isArray() ? 'yes' : 'no')."\n"
+ .'- **Default**: ``'.str_replace("\n", '', var_export($argument->getDefault(), true)).'``'
+ );
+ }
+
+ protected function describeInputOption(InputOption $option, array $options = []): void
+ {
+ $name = '\-\-'.$option->getName();
+ if ($option->isNegatable()) {
+ $name .= '|\-\-no-'.$option->getName();
+ }
+ if ($option->getShortcut()) {
+ $name .= '|-'.str_replace('|', '|-', $option->getShortcut());
+ }
+
+ $optionDescription = $option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n\n", $option->getDescription())."\n\n" : '';
+ $optionDescription = (new UnicodeString($optionDescription))->ascii();
+ $this->write(
+ $name."\n".str_repeat($this->paragraphsChar, Helper::width($name))."\n\n"
+ .$optionDescription
+ .'- **Accept value**: '.($option->acceptValue() ? 'yes' : 'no')."\n"
+ .'- **Is value required**: '.($option->isValueRequired() ? 'yes' : 'no')."\n"
+ .'- **Is multiple**: '.($option->isArray() ? 'yes' : 'no')."\n"
+ .'- **Is negatable**: '.($option->isNegatable() ? 'yes' : 'no')."\n"
+ .'- **Default**: ``'.str_replace("\n", '', var_export($option->getDefault(), true)).'``'."\n"
+ );
+ }
+
+ protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
+ {
+ if ($showArguments = ((bool) $definition->getArguments())) {
+ $this->write("Arguments\n".str_repeat($this->subsubsectionChar, 9));
+ foreach ($definition->getArguments() as $argument) {
+ $this->write("\n\n");
+ $this->describeInputArgument($argument);
+ }
+ }
+
+ if ($nonDefaultOptions = $this->getNonDefaultOptions($definition)) {
+ if ($showArguments) {
+ $this->write("\n\n");
+ }
+
+ $this->write("Options\n".str_repeat($this->subsubsectionChar, 7)."\n\n");
+ foreach ($nonDefaultOptions as $option) {
+ $this->describeInputOption($option);
+ $this->write("\n");
+ }
+ }
+ }
+
+ protected function describeCommand(Command $command, array $options = []): void
+ {
+ if ($options['short'] ?? false) {
+ $this->write(
+ '``'.$command->getName()."``\n"
+ .str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n"
+ .($command->getDescription() ? $command->getDescription()."\n\n" : '')
+ ."Usage\n".str_repeat($this->paragraphsChar, 5)."\n\n"
+ .array_reduce($command->getAliases(), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n")
+ );
+
+ return;
+ }
+
+ $command->mergeApplicationDefinition(false);
+
+ foreach ($command->getAliases() as $alias) {
+ $this->write('.. _'.$alias.":\n\n");
+ }
+ $this->write(
+ $command->getName()."\n"
+ .str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n"
+ .($command->getDescription() ? $command->getDescription()."\n\n" : '')
+ ."Usage\n".str_repeat($this->subsubsectionChar, 5)."\n\n"
+ .array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n")
+ );
+
+ if ($help = $command->getProcessedHelp()) {
+ $this->write("\n");
+ $this->write($help);
+ }
+
+ $definition = $command->getDefinition();
+ if ($definition->getOptions() || $definition->getArguments()) {
+ $this->write("\n\n");
+ $this->describeInputDefinition($definition);
+ }
+ }
+
+ protected function describeApplication(Application $application, array $options = []): void
+ {
+ $description = new ApplicationDescription($application, $options['namespace'] ?? null);
+ $title = $this->getApplicationTitle($application);
+
+ $this->write($title."\n".str_repeat($this->partChar, Helper::width($title)));
+ $this->createTableOfContents($description, $application);
+ $this->describeCommands($application, $options);
+ }
+
+ private function getApplicationTitle(Application $application): string
+ {
+ if ('UNKNOWN' === $application->getName()) {
+ return 'Console Tool';
+ }
+ if ('UNKNOWN' !== $application->getVersion()) {
+ return \sprintf('%s %s', $application->getName(), $application->getVersion());
+ }
+
+ return $application->getName();
+ }
+
+ private function describeCommands($application, array $options): void
+ {
+ $title = 'Commands';
+ $this->write("\n\n$title\n".str_repeat($this->chapterChar, Helper::width($title))."\n\n");
+ foreach ($this->visibleNamespaces as $namespace) {
+ if ('_global' === $namespace) {
+ $commands = $application->all('');
+ $this->write('Global'."\n".str_repeat($this->sectionChar, Helper::width('Global'))."\n\n");
+ } else {
+ $commands = $application->all($namespace);
+ $this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n");
+ }
+
+ foreach ($this->removeAliasesAndHiddenCommands($commands) as $command) {
+ $this->describeCommand($command, $options);
+ $this->write("\n\n");
+ }
+ }
+ }
+
+ private function createTableOfContents(ApplicationDescription $description, Application $application): void
+ {
+ $this->setVisibleNamespaces($description);
+ $chapterTitle = 'Table of Contents';
+ $this->write("\n\n$chapterTitle\n".str_repeat($this->chapterChar, Helper::width($chapterTitle))."\n\n");
+ foreach ($this->visibleNamespaces as $namespace) {
+ if ('_global' === $namespace) {
+ $commands = $application->all('');
+ } else {
+ $commands = $application->all($namespace);
+ $this->write("\n\n");
+ $this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n");
+ }
+ $commands = $this->removeAliasesAndHiddenCommands($commands);
+
+ $this->write("\n\n");
+ $this->write(implode("\n", array_map(static fn ($commandName) => \sprintf('- `%s`_', $commandName), array_keys($commands))));
+ }
+ }
+
+ private function getNonDefaultOptions(InputDefinition $definition): array
+ {
+ $globalOptions = [
+ 'help',
+ 'silent',
+ 'quiet',
+ 'verbose',
+ 'version',
+ 'ansi',
+ 'no-interaction',
+ ];
+ $nonDefaultOptions = [];
+ foreach ($definition->getOptions() as $option) {
+ // Skip global options.
+ if (!\in_array($option->getName(), $globalOptions, true)) {
+ $nonDefaultOptions[] = $option;
+ }
+ }
+
+ return $nonDefaultOptions;
+ }
+
+ private function setVisibleNamespaces(ApplicationDescription $description): void
+ {
+ $commands = $description->getCommands();
+ foreach ($description->getNamespaces() as $namespace) {
+ try {
+ $namespaceCommands = $namespace['commands'];
+ foreach ($namespaceCommands as $key => $commandName) {
+ if (!\array_key_exists($commandName, $commands)) {
+ // If the array key does not exist, then this is an alias.
+ unset($namespaceCommands[$key]);
+ } elseif ($commands[$commandName]->isHidden()) {
+ unset($namespaceCommands[$key]);
+ }
+ }
+ if (!$namespaceCommands) {
+ // If the namespace contained only aliases or hidden commands, skip the namespace.
+ continue;
+ }
+ } catch (\Exception) {
+ }
+ $this->visibleNamespaces[] = $namespace['id'];
+ }
+ }
+
+ private function removeAliasesAndHiddenCommands(array $commands): array
+ {
+ foreach ($commands as $key => $command) {
+ if ($command->isHidden() || \in_array($key, $command->getAliases(), true)) {
+ unset($commands[$key]);
+ }
+ }
+ unset($commands['completion']);
+
+ return $commands;
+ }
+}
diff --git a/Descriptor/TextDescriptor.php b/Descriptor/TextDescriptor.php
index fbb140ae7..51c411f46 100644
--- a/Descriptor/TextDescriptor.php
+++ b/Descriptor/TextDescriptor.php
@@ -28,13 +28,10 @@
*/
class TextDescriptor extends Descriptor
{
- /**
- * {@inheritdoc}
- */
- protected function describeInputArgument(InputArgument $argument, array $options = [])
+ protected function describeInputArgument(InputArgument $argument, array $options = []): void
{
if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) {
- $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault()));
+ $default = \sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault()));
} else {
$default = '';
}
@@ -42,7 +39,7 @@ protected function describeInputArgument(InputArgument $argument, array $options
$totalWidth = $options['total_width'] ?? Helper::width($argument->getName());
$spacingWidth = $totalWidth - \strlen($argument->getName());
- $this->writeText(sprintf(' %s %s%s%s',
+ $this->writeText(\sprintf(' %s %s%s%s',
$argument->getName(),
str_repeat(' ', $spacingWidth),
// + 4 = 2 spaces before , 2 spaces after
@@ -51,13 +48,10 @@ protected function describeInputArgument(InputArgument $argument, array $options
), $options);
}
- /**
- * {@inheritdoc}
- */
- protected function describeInputOption(InputOption $option, array $options = [])
+ protected function describeInputOption(InputOption $option, array $options = []): void
{
if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) {
- $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault()));
+ $default = \sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault()));
} else {
$default = '';
}
@@ -72,14 +66,14 @@ protected function describeInputOption(InputOption $option, array $options = [])
}
$totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]);
- $synopsis = sprintf('%s%s',
- $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ',
- sprintf($option->isNegatable() ? '--%1$s|--no-%1$s' : '--%1$s%2$s', $option->getName(), $value)
+ $synopsis = \sprintf('%s%s',
+ $option->getShortcut() ? \sprintf('-%s, ', $option->getShortcut()) : ' ',
+ \sprintf($option->isNegatable() ? '--%1$s|--no-%1$s' : '--%1$s%2$s', $option->getName(), $value)
);
$spacingWidth = $totalWidth - Helper::width($synopsis);
- $this->writeText(sprintf(' %s %s%s%s%s',
+ $this->writeText(\sprintf(' %s %s%s%s%s',
$synopsis,
str_repeat(' ', $spacingWidth),
// + 4 = 2 spaces before , 2 spaces after
@@ -89,10 +83,7 @@ protected function describeInputOption(InputOption $option, array $options = [])
), $options);
}
- /**
- * {@inheritdoc}
- */
- protected function describeInputDefinition(InputDefinition $definition, array $options = [])
+ protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
{
$totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
foreach ($definition->getArguments() as $argument) {
@@ -131,10 +122,7 @@ protected function describeInputDefinition(InputDefinition $definition, array $o
}
}
- /**
- * {@inheritdoc}
- */
- protected function describeCommand(Command $command, array $options = [])
+ protected function describeCommand(Command $command, array $options = []): void
{
$command->mergeApplicationDefinition(false);
@@ -169,10 +157,7 @@ protected function describeCommand(Command $command, array $options = [])
}
}
- /**
- * {@inheritdoc}
- */
- protected function describeApplication(Application $application, array $options = [])
+ protected function describeApplication(Application $application, array $options = []): void
{
$describedNamespace = $options['namespace'] ?? null;
$description = new ApplicationDescription($application, $describedNamespace);
@@ -181,7 +166,7 @@ protected function describeApplication(Application $application, array $options
$width = $this->getColumnWidth($description->getCommands());
foreach ($description->getCommands() as $command) {
- $this->writeText(sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options);
+ $this->writeText(\sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options);
$this->writeText("\n");
}
} else {
@@ -208,20 +193,16 @@ protected function describeApplication(Application $application, array $options
}
// calculate max. width based on available commands per namespace
- $width = $this->getColumnWidth(array_merge(...array_values(array_map(function ($namespace) use ($commands) {
- return array_intersect($namespace['commands'], array_keys($commands));
- }, array_values($namespaces)))));
+ $width = $this->getColumnWidth(array_merge(...array_values(array_map(fn ($namespace) => array_intersect($namespace['commands'], array_keys($commands)), array_values($namespaces)))));
if ($describedNamespace) {
- $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options);
+ $this->writeText(\sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options);
} else {
$this->writeText('Available commands:', $options);
}
foreach ($namespaces as $namespace) {
- $namespace['commands'] = array_filter($namespace['commands'], function ($name) use ($commands) {
- return isset($commands[$name]);
- });
+ $namespace['commands'] = array_filter($namespace['commands'], fn ($name) => isset($commands[$name]));
if (!$namespace['commands']) {
continue;
@@ -237,7 +218,7 @@ protected function describeApplication(Application $application, array $options
$spacingWidth = $width - Helper::width($name);
$command = $commands[$name];
$commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : '';
- $this->writeText(sprintf(' %s%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options);
+ $this->writeText(\sprintf(' %s%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options);
}
}
@@ -245,10 +226,7 @@ protected function describeApplication(Application $application, array $options
}
}
- /**
- * {@inheritdoc}
- */
- private function writeText(string $content, array $options = [])
+ private function writeText(string $content, array $options = []): void
{
$this->write(
isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content,
@@ -273,10 +251,8 @@ private function getCommandAliasesText(Command $command): string
/**
* Formats input option/argument default value.
- *
- * @param mixed $default
*/
- private function formatDefaultValue($default): string
+ private function formatDefaultValue(mixed $default): string
{
if (\INF === $default) {
return 'INF';
diff --git a/Descriptor/XmlDescriptor.php b/Descriptor/XmlDescriptor.php
index f17e5f1f2..00055557c 100644
--- a/Descriptor/XmlDescriptor.php
+++ b/Descriptor/XmlDescriptor.php
@@ -120,42 +120,27 @@ public function getApplicationDocument(Application $application, ?string $namesp
return $dom;
}
- /**
- * {@inheritdoc}
- */
- protected function describeInputArgument(InputArgument $argument, array $options = [])
+ protected function describeInputArgument(InputArgument $argument, array $options = []): void
{
$this->writeDocument($this->getInputArgumentDocument($argument));
}
- /**
- * {@inheritdoc}
- */
- protected function describeInputOption(InputOption $option, array $options = [])
+ protected function describeInputOption(InputOption $option, array $options = []): void
{
$this->writeDocument($this->getInputOptionDocument($option));
}
- /**
- * {@inheritdoc}
- */
- protected function describeInputDefinition(InputDefinition $definition, array $options = [])
+ protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
{
$this->writeDocument($this->getInputDefinitionDocument($definition));
}
- /**
- * {@inheritdoc}
- */
- protected function describeCommand(Command $command, array $options = [])
+ protected function describeCommand(Command $command, array $options = []): void
{
$this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false));
}
- /**
- * {@inheritdoc}
- */
- protected function describeApplication(Application $application, array $options = [])
+ protected function describeApplication(Application $application, array $options = []): void
{
$this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false));
}
@@ -163,7 +148,7 @@ protected function describeApplication(Application $application, array $options
/**
* Appends document children to parent node.
*/
- private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent)
+ private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent): void
{
foreach ($importedParent->childNodes as $childNode) {
$parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true));
@@ -173,7 +158,7 @@ private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent)
/**
* Writes DOM document.
*/
- private function writeDocument(\DOMDocument $dom)
+ private function writeDocument(\DOMDocument $dom): void
{
$dom->formatOutput = true;
$this->write($dom->saveXML());
@@ -223,11 +208,9 @@ private function getInputOptionDocument(InputOption $option): \DOMDocument
$defaults = \is_array($option->getDefault()) ? $option->getDefault() : (\is_bool($option->getDefault()) ? [var_export($option->getDefault(), true)] : ($option->getDefault() ? [$option->getDefault()] : []));
$objectXML->appendChild($defaultsXML = $dom->createElement('defaults'));
- if (!empty($defaults)) {
- foreach ($defaults as $default) {
- $defaultsXML->appendChild($defaultXML = $dom->createElement('default'));
- $defaultXML->appendChild($dom->createTextNode($default));
- }
+ foreach ($defaults as $default) {
+ $defaultsXML->appendChild($defaultXML = $dom->createElement('default'));
+ $defaultXML->appendChild($dom->createTextNode($default));
}
}
diff --git a/Event/ConsoleAlarmEvent.php b/Event/ConsoleAlarmEvent.php
new file mode 100644
index 000000000..876ab59b9
--- /dev/null
+++ b/Event/ConsoleAlarmEvent.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Event;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+final class ConsoleAlarmEvent extends ConsoleEvent
+{
+ public function __construct(
+ Command $command,
+ InputInterface $input,
+ OutputInterface $output,
+ private int|false $exitCode = 0,
+ ) {
+ parent::__construct($command, $input, $output);
+ }
+
+ public function setExitCode(int $exitCode): void
+ {
+ if ($exitCode < 0 || $exitCode > 255) {
+ throw new \InvalidArgumentException('Exit code must be between 0 and 255.');
+ }
+
+ $this->exitCode = $exitCode;
+ }
+
+ public function abortExit(): void
+ {
+ $this->exitCode = false;
+ }
+
+ public function getExitCode(): int|false
+ {
+ return $this->exitCode;
+ }
+}
diff --git a/Event/ConsoleCommandEvent.php b/Event/ConsoleCommandEvent.php
index 1b4f9f9b1..0757a23f6 100644
--- a/Event/ConsoleCommandEvent.php
+++ b/Event/ConsoleCommandEvent.php
@@ -29,7 +29,7 @@ final class ConsoleCommandEvent extends ConsoleEvent
/**
* Indicates if the command should be run or skipped.
*/
- private $commandShouldRun = true;
+ private bool $commandShouldRun = true;
/**
* Disables the command, so it won't be run.
diff --git a/Event/ConsoleErrorEvent.php b/Event/ConsoleErrorEvent.php
index d4c26493f..1c0d62652 100644
--- a/Event/ConsoleErrorEvent.php
+++ b/Event/ConsoleErrorEvent.php
@@ -22,14 +22,15 @@
*/
final class ConsoleErrorEvent extends ConsoleEvent
{
- private $error;
- private $exitCode;
-
- public function __construct(InputInterface $input, OutputInterface $output, \Throwable $error, ?Command $command = null)
- {
+ private int $exitCode;
+
+ public function __construct(
+ InputInterface $input,
+ OutputInterface $output,
+ private \Throwable $error,
+ ?Command $command = null,
+ ) {
parent::__construct($command, $input, $output);
-
- $this->error = $error;
}
public function getError(): \Throwable
@@ -47,7 +48,6 @@ public function setExitCode(int $exitCode): void
$this->exitCode = $exitCode;
$r = new \ReflectionProperty($this->error, 'code');
- $r->setAccessible(true);
$r->setValue($this->error, $this->exitCode);
}
diff --git a/Event/ConsoleEvent.php b/Event/ConsoleEvent.php
index be7937d51..2f9f0778e 100644
--- a/Event/ConsoleEvent.php
+++ b/Event/ConsoleEvent.php
@@ -23,44 +23,33 @@
*/
class ConsoleEvent extends Event
{
- protected $command;
-
- private $input;
- private $output;
-
- public function __construct(?Command $command, InputInterface $input, OutputInterface $output)
- {
- $this->command = $command;
- $this->input = $input;
- $this->output = $output;
+ public function __construct(
+ protected ?Command $command,
+ private InputInterface $input,
+ private OutputInterface $output,
+ ) {
}
/**
* Gets the command that is executed.
- *
- * @return Command|null
*/
- public function getCommand()
+ public function getCommand(): ?Command
{
return $this->command;
}
/**
* Gets the input instance.
- *
- * @return InputInterface
*/
- public function getInput()
+ public function getInput(): InputInterface
{
return $this->input;
}
/**
* Gets the output instance.
- *
- * @return OutputInterface
*/
- public function getOutput()
+ public function getOutput(): OutputInterface
{
return $this->output;
}
diff --git a/Event/ConsoleSignalEvent.php b/Event/ConsoleSignalEvent.php
index ef13ed2f5..b27f08a18 100644
--- a/Event/ConsoleSignalEvent.php
+++ b/Event/ConsoleSignalEvent.php
@@ -20,16 +20,37 @@
*/
final class ConsoleSignalEvent extends ConsoleEvent
{
- private $handlingSignal;
-
- public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal)
- {
+ public function __construct(
+ Command $command,
+ InputInterface $input,
+ OutputInterface $output,
+ private int $handlingSignal,
+ private int|false $exitCode = 0,
+ ) {
parent::__construct($command, $input, $output);
- $this->handlingSignal = $handlingSignal;
}
public function getHandlingSignal(): int
{
return $this->handlingSignal;
}
+
+ public function setExitCode(int $exitCode): void
+ {
+ if ($exitCode < 0 || $exitCode > 255) {
+ throw new \InvalidArgumentException('Exit code must be between 0 and 255.');
+ }
+
+ $this->exitCode = $exitCode;
+ }
+
+ public function abortExit(): void
+ {
+ $this->exitCode = false;
+ }
+
+ public function getExitCode(): int|false
+ {
+ return $this->exitCode;
+ }
}
diff --git a/Event/ConsoleTerminateEvent.php b/Event/ConsoleTerminateEvent.php
index 190038d1a..38f7253a5 100644
--- a/Event/ConsoleTerminateEvent.php
+++ b/Event/ConsoleTerminateEvent.php
@@ -19,16 +19,18 @@
* Allows to manipulate the exit code of a command after its execution.
*
* @author Francesco Levorato
+ * @author Jules Pietri
*/
final class ConsoleTerminateEvent extends ConsoleEvent
{
- private $exitCode;
-
- public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $exitCode)
- {
+ public function __construct(
+ Command $command,
+ InputInterface $input,
+ OutputInterface $output,
+ private int $exitCode,
+ private readonly ?int $interruptingSignal = null,
+ ) {
parent::__construct($command, $input, $output);
-
- $this->setExitCode($exitCode);
}
public function setExitCode(int $exitCode): void
@@ -40,4 +42,9 @@ public function getExitCode(): int
{
return $this->exitCode;
}
+
+ public function getInterruptingSignal(): ?int
+ {
+ return $this->interruptingSignal;
+ }
}
diff --git a/EventListener/ErrorListener.php b/EventListener/ErrorListener.php
index e9c9e3ea4..9acb0e41d 100644
--- a/EventListener/ErrorListener.php
+++ b/EventListener/ErrorListener.php
@@ -24,14 +24,12 @@
*/
class ErrorListener implements EventSubscriberInterface
{
- private $logger;
-
- public function __construct(?LoggerInterface $logger = null)
- {
- $this->logger = $logger;
+ public function __construct(
+ private ?LoggerInterface $logger = null,
+ ) {
}
- public function onConsoleError(ConsoleErrorEvent $event)
+ public function onConsoleError(ConsoleErrorEvent $event): void
{
if (null === $this->logger) {
return;
@@ -39,7 +37,7 @@ public function onConsoleError(ConsoleErrorEvent $event)
$error = $event->getError();
- if (!$inputString = $this->getInputString($event)) {
+ if (!$inputString = self::getInputString($event)) {
$this->logger->critical('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]);
return;
@@ -48,7 +46,7 @@ public function onConsoleError(ConsoleErrorEvent $event)
$this->logger->critical('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]);
}
- public function onConsoleTerminate(ConsoleTerminateEvent $event)
+ public function onConsoleTerminate(ConsoleTerminateEvent $event): void
{
if (null === $this->logger) {
return;
@@ -60,7 +58,7 @@ public function onConsoleTerminate(ConsoleTerminateEvent $event)
return;
}
- if (!$inputString = $this->getInputString($event)) {
+ if (!$inputString = self::getInputString($event)) {
$this->logger->debug('The console exited with code "{code}"', ['code' => $exitCode]);
return;
@@ -69,7 +67,7 @@ public function onConsoleTerminate(ConsoleTerminateEvent $event)
$this->logger->debug('Command "{command}" exited with code "{code}"', ['command' => $inputString, 'code' => $exitCode]);
}
- public static function getSubscribedEvents()
+ public static function getSubscribedEvents(): array
{
return [
ConsoleEvents::ERROR => ['onConsoleError', -128],
@@ -77,19 +75,15 @@ public static function getSubscribedEvents()
];
}
- private static function getInputString(ConsoleEvent $event): ?string
+ private static function getInputString(ConsoleEvent $event): string
{
- $commandName = $event->getCommand() ? $event->getCommand()->getName() : null;
- $input = $event->getInput();
-
- if (method_exists($input, '__toString')) {
- if ($commandName) {
- return str_replace(["'$commandName'", "\"$commandName\""], $commandName, (string) $input);
- }
+ $commandName = $event->getCommand()?->getName();
+ $inputString = (string) $event->getInput();
- return (string) $input;
+ if ($commandName) {
+ return str_replace(["'$commandName'", "\"$commandName\""], $commandName, $inputString);
}
- return $commandName;
+ return $inputString;
}
}
diff --git a/Exception/CommandNotFoundException.php b/Exception/CommandNotFoundException.php
index 81ec318ab..246f04fa2 100644
--- a/Exception/CommandNotFoundException.php
+++ b/Exception/CommandNotFoundException.php
@@ -18,25 +18,25 @@
*/
class CommandNotFoundException extends \InvalidArgumentException implements ExceptionInterface
{
- private $alternatives;
-
/**
* @param string $message Exception message to throw
* @param string[] $alternatives List of similar defined names
* @param int $code Exception code
* @param \Throwable|null $previous Previous exception used for the exception chaining
*/
- public function __construct(string $message, array $alternatives = [], int $code = 0, ?\Throwable $previous = null)
- {
+ public function __construct(
+ string $message,
+ private array $alternatives = [],
+ int $code = 0,
+ ?\Throwable $previous = null,
+ ) {
parent::__construct($message, $code, $previous);
-
- $this->alternatives = $alternatives;
}
/**
* @return string[]
*/
- public function getAlternatives()
+ public function getAlternatives(): array
{
return $this->alternatives;
}
diff --git a/Exception/RunCommandFailedException.php b/Exception/RunCommandFailedException.php
new file mode 100644
index 000000000..5d87ec949
--- /dev/null
+++ b/Exception/RunCommandFailedException.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Exception;
+
+use Symfony\Component\Console\Messenger\RunCommandContext;
+
+/**
+ * @author Kevin Bond
+ */
+final class RunCommandFailedException extends RuntimeException
+{
+ public function __construct(\Throwable|string $exception, public readonly RunCommandContext $context)
+ {
+ parent::__construct(
+ $exception instanceof \Throwable ? $exception->getMessage() : $exception,
+ $exception instanceof \Throwable ? $exception->getCode() : 0,
+ $exception instanceof \Throwable ? $exception : null,
+ );
+ }
+}
diff --git a/Formatter/NullOutputFormatter.php b/Formatter/NullOutputFormatter.php
index d770e1465..5c11c7644 100644
--- a/Formatter/NullOutputFormatter.php
+++ b/Formatter/NullOutputFormatter.php
@@ -16,52 +16,34 @@
*/
final class NullOutputFormatter implements OutputFormatterInterface
{
- private $style;
+ private NullOutputFormatterStyle $style;
- /**
- * {@inheritdoc}
- */
public function format(?string $message): ?string
{
return null;
}
- /**
- * {@inheritdoc}
- */
public function getStyle(string $name): OutputFormatterStyleInterface
{
// to comply with the interface we must return a OutputFormatterStyleInterface
- return $this->style ?? $this->style = new NullOutputFormatterStyle();
+ return $this->style ??= new NullOutputFormatterStyle();
}
- /**
- * {@inheritdoc}
- */
public function hasStyle(string $name): bool
{
return false;
}
- /**
- * {@inheritdoc}
- */
public function isDecorated(): bool
{
return false;
}
- /**
- * {@inheritdoc}
- */
public function setDecorated(bool $decorated): void
{
// do nothing
}
- /**
- * {@inheritdoc}
- */
public function setStyle(string $name, OutputFormatterStyleInterface $style): void
{
// do nothing
diff --git a/Formatter/NullOutputFormatterStyle.php b/Formatter/NullOutputFormatterStyle.php
index afd3d0043..06fa6e40b 100644
--- a/Formatter/NullOutputFormatterStyle.php
+++ b/Formatter/NullOutputFormatterStyle.php
@@ -16,49 +16,31 @@
*/
final class NullOutputFormatterStyle implements OutputFormatterStyleInterface
{
- /**
- * {@inheritdoc}
- */
public function apply(string $text): string
{
return $text;
}
- /**
- * {@inheritdoc}
- */
- public function setBackground(?string $color = null): void
+ public function setBackground(?string $color): void
{
// do nothing
}
- /**
- * {@inheritdoc}
- */
- public function setForeground(?string $color = null): void
+ public function setForeground(?string $color): void
{
// do nothing
}
- /**
- * {@inheritdoc}
- */
public function setOption(string $option): void
{
// do nothing
}
- /**
- * {@inheritdoc}
- */
public function setOptions(array $options): void
{
// do nothing
}
- /**
- * {@inheritdoc}
- */
public function unsetOption(string $option): void
{
// do nothing
diff --git a/Formatter/OutputFormatter.php b/Formatter/OutputFormatter.php
index 4ec600244..3c8c287e8 100644
--- a/Formatter/OutputFormatter.php
+++ b/Formatter/OutputFormatter.php
@@ -23,9 +23,8 @@
*/
class OutputFormatter implements WrappableOutputFormatterInterface
{
- private $decorated;
- private $styles = [];
- private $styleStack;
+ private array $styles = [];
+ private OutputFormatterStyleStack $styleStack;
public function __clone()
{
@@ -37,10 +36,8 @@ public function __clone()
/**
* Escapes "<" and ">" special chars in given text.
- *
- * @return string
*/
- public static function escape(string $text)
+ public static function escape(string $text): string
{
$text = preg_replace('/([^\\\\]|^)([<>])/', '$1\\\\$2', $text);
@@ -69,10 +66,10 @@ public static function escapeTrailingBackslash(string $text): string
*
* @param OutputFormatterStyleInterface[] $styles Array of "name => FormatterStyle" instances
*/
- public function __construct(bool $decorated = false, array $styles = [])
- {
- $this->decorated = $decorated;
-
+ public function __construct(
+ private bool $decorated = false,
+ array $styles = [],
+ ) {
$this->setStyle('error', new OutputFormatterStyle('white', 'red'));
$this->setStyle('info', new OutputFormatterStyle('green'));
$this->setStyle('comment', new OutputFormatterStyle('yellow'));
@@ -85,62 +82,41 @@ public function __construct(bool $decorated = false, array $styles = [])
$this->styleStack = new OutputFormatterStyleStack();
}
- /**
- * {@inheritdoc}
- */
- public function setDecorated(bool $decorated)
+ public function setDecorated(bool $decorated): void
{
$this->decorated = $decorated;
}
- /**
- * {@inheritdoc}
- */
- public function isDecorated()
+ public function isDecorated(): bool
{
return $this->decorated;
}
- /**
- * {@inheritdoc}
- */
- public function setStyle(string $name, OutputFormatterStyleInterface $style)
+ public function setStyle(string $name, OutputFormatterStyleInterface $style): void
{
$this->styles[strtolower($name)] = $style;
}
- /**
- * {@inheritdoc}
- */
- public function hasStyle(string $name)
+ public function hasStyle(string $name): bool
{
return isset($this->styles[strtolower($name)]);
}
- /**
- * {@inheritdoc}
- */
- public function getStyle(string $name)
+ public function getStyle(string $name): OutputFormatterStyleInterface
{
if (!$this->hasStyle($name)) {
- throw new InvalidArgumentException(sprintf('Undefined style: "%s".', $name));
+ throw new InvalidArgumentException(\sprintf('Undefined style: "%s".', $name));
}
return $this->styles[strtolower($name)];
}
- /**
- * {@inheritdoc}
- */
- public function format(?string $message)
+ public function format(?string $message): ?string
{
return $this->formatAndWrap($message, 0);
}
- /**
- * {@inheritdoc}
- */
- public function formatAndWrap(?string $message, int $width)
+ public function formatAndWrap(?string $message, int $width): string
{
if (null === $message) {
return '';
@@ -165,7 +141,7 @@ public function formatAndWrap(?string $message, int $width)
$offset = $pos + \strlen($text);
// opening tag?
- if ($open = '/' != $text[1]) {
+ if ($open = '/' !== $text[1]) {
$tag = $matches[1][$i][0];
} else {
$tag = $matches[3][$i][0] ?? '';
@@ -188,10 +164,7 @@ public function formatAndWrap(?string $message, int $width)
return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']);
}
- /**
- * @return OutputFormatterStyleStack
- */
- public function getStyleStack()
+ public function getStyleStack(): OutputFormatterStyleStack
{
return $this->styleStack;
}
@@ -263,7 +236,7 @@ private function applyCurrentStyle(string $text, string $current, int $width, in
$text = $prefix.$this->addLineBreaks($text, $width);
$text = rtrim($text, "\n").($matches[1] ?? '');
- if (!$currentLineLength && '' !== $current && "\n" !== substr($current, -1)) {
+ if (!$currentLineLength && '' !== $current && !str_ends_with($current, "\n")) {
$text = "\n".$text;
}
diff --git a/Formatter/OutputFormatterInterface.php b/Formatter/OutputFormatterInterface.php
index 0b5f839a2..947347fa7 100644
--- a/Formatter/OutputFormatterInterface.php
+++ b/Formatter/OutputFormatterInterface.php
@@ -21,40 +21,32 @@ interface OutputFormatterInterface
/**
* Sets the decorated flag.
*/
- public function setDecorated(bool $decorated);
+ public function setDecorated(bool $decorated): void;
/**
* Whether the output will decorate messages.
- *
- * @return bool
*/
- public function isDecorated();
+ public function isDecorated(): bool;
/**
* Sets a new style.
*/
- public function setStyle(string $name, OutputFormatterStyleInterface $style);
+ public function setStyle(string $name, OutputFormatterStyleInterface $style): void;
/**
* Checks if output formatter has style with specified name.
- *
- * @return bool
*/
- public function hasStyle(string $name);
+ public function hasStyle(string $name): bool;
/**
* Gets style options from style with specified name.
*
- * @return OutputFormatterStyleInterface
- *
* @throws \InvalidArgumentException When style isn't defined
*/
- public function getStyle(string $name);
+ public function getStyle(string $name): OutputFormatterStyleInterface;
/**
* Formats a message according to the given styles.
- *
- * @return string|null
*/
- public function format(?string $message);
+ public function format(?string $message): ?string;
}
diff --git a/Formatter/OutputFormatterStyle.php b/Formatter/OutputFormatterStyle.php
index d7ae66494..20a65b517 100644
--- a/Formatter/OutputFormatterStyle.php
+++ b/Formatter/OutputFormatterStyle.php
@@ -20,12 +20,12 @@
*/
class OutputFormatterStyle implements OutputFormatterStyleInterface
{
- private $color;
- private $foreground;
- private $background;
- private $options;
- private $href;
- private $handlesHrefGracefully;
+ private Color $color;
+ private string $foreground;
+ private string $background;
+ private array $options;
+ private ?string $href = null;
+ private bool $handlesHrefGracefully;
/**
* Initializes output formatter style.
@@ -38,18 +38,12 @@ public function __construct(?string $foreground = null, ?string $background = nu
$this->color = new Color($this->foreground = $foreground ?: '', $this->background = $background ?: '', $this->options = $options);
}
- /**
- * {@inheritdoc}
- */
- public function setForeground(?string $color = null)
+ public function setForeground(?string $color): void
{
$this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options);
}
- /**
- * {@inheritdoc}
- */
- public function setBackground(?string $color = null)
+ public function setBackground(?string $color): void
{
$this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options);
}
@@ -59,19 +53,13 @@ public function setHref(string $url): void
$this->href = $url;
}
- /**
- * {@inheritdoc}
- */
- public function setOption(string $option)
+ public function setOption(string $option): void
{
$this->options[] = $option;
$this->color = new Color($this->foreground, $this->background, $this->options);
}
- /**
- * {@inheritdoc}
- */
- public function unsetOption(string $option)
+ public function unsetOption(string $option): void
{
$pos = array_search($option, $this->options);
if (false !== $pos) {
@@ -81,24 +69,16 @@ public function unsetOption(string $option)
$this->color = new Color($this->foreground, $this->background, $this->options);
}
- /**
- * {@inheritdoc}
- */
- public function setOptions(array $options)
+ public function setOptions(array $options): void
{
$this->color = new Color($this->foreground, $this->background, $this->options = $options);
}
- /**
- * {@inheritdoc}
- */
- public function apply(string $text)
+ public function apply(string $text): string
{
- if (null === $this->handlesHrefGracefully) {
- $this->handlesHrefGracefully = 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR')
- && (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100)
- && !isset($_SERVER['IDEA_INITIAL_DIRECTORY']);
- }
+ $this->handlesHrefGracefully ??= 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR')
+ && (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100)
+ && !isset($_SERVER['IDEA_INITIAL_DIRECTORY']);
if (null !== $this->href && $this->handlesHrefGracefully) {
$text = "\033]8;;$this->href\033\\$text\033]8;;\033\\";
diff --git a/Formatter/OutputFormatterStyleInterface.php b/Formatter/OutputFormatterStyleInterface.php
index 89e4d2438..037419277 100644
--- a/Formatter/OutputFormatterStyleInterface.php
+++ b/Formatter/OutputFormatterStyleInterface.php
@@ -21,32 +21,30 @@ interface OutputFormatterStyleInterface
/**
* Sets style foreground color.
*/
- public function setForeground(?string $color = null);
+ public function setForeground(?string $color): void;
/**
* Sets style background color.
*/
- public function setBackground(?string $color = null);
+ public function setBackground(?string $color): void;
/**
* Sets some specific style option.
*/
- public function setOption(string $option);
+ public function setOption(string $option): void;
/**
* Unsets some specific style option.
*/
- public function unsetOption(string $option);
+ public function unsetOption(string $option): void;
/**
* Sets multiple style options at once.
*/
- public function setOptions(array $options);
+ public function setOptions(array $options): void;
/**
* Applies the style to a given text.
- *
- * @return string
*/
- public function apply(string $text);
+ public function apply(string $text): string;
}
diff --git a/Formatter/OutputFormatterStyleStack.php b/Formatter/OutputFormatterStyleStack.php
index 1b9356301..4985213ab 100644
--- a/Formatter/OutputFormatterStyleStack.php
+++ b/Formatter/OutputFormatterStyleStack.php
@@ -22,9 +22,9 @@ class OutputFormatterStyleStack implements ResetInterface
/**
* @var OutputFormatterStyleInterface[]
*/
- private $styles;
+ private array $styles = [];
- private $emptyStyle;
+ private OutputFormatterStyleInterface $emptyStyle;
public function __construct(?OutputFormatterStyleInterface $emptyStyle = null)
{
@@ -35,7 +35,7 @@ public function __construct(?OutputFormatterStyleInterface $emptyStyle = null)
/**
* Resets stack (ie. empty internal arrays).
*/
- public function reset()
+ public function reset(): void
{
$this->styles = [];
}
@@ -43,7 +43,7 @@ public function reset()
/**
* Pushes a style in the stack.
*/
- public function push(OutputFormatterStyleInterface $style)
+ public function push(OutputFormatterStyleInterface $style): void
{
$this->styles[] = $style;
}
@@ -51,13 +51,11 @@ public function push(OutputFormatterStyleInterface $style)
/**
* Pops a style from the stack.
*
- * @return OutputFormatterStyleInterface
- *
* @throws InvalidArgumentException When style tags incorrectly nested
*/
- public function pop(?OutputFormatterStyleInterface $style = null)
+ public function pop(?OutputFormatterStyleInterface $style = null): OutputFormatterStyleInterface
{
- if (empty($this->styles)) {
+ if (!$this->styles) {
return $this->emptyStyle;
}
@@ -78,12 +76,10 @@ public function pop(?OutputFormatterStyleInterface $style = null)
/**
* Computes current style with stacks top codes.
- *
- * @return OutputFormatterStyle
*/
- public function getCurrent()
+ public function getCurrent(): OutputFormatterStyleInterface
{
- if (empty($this->styles)) {
+ if (!$this->styles) {
return $this->emptyStyle;
}
@@ -93,17 +89,14 @@ public function getCurrent()
/**
* @return $this
*/
- public function setEmptyStyle(OutputFormatterStyleInterface $emptyStyle)
+ public function setEmptyStyle(OutputFormatterStyleInterface $emptyStyle): static
{
$this->emptyStyle = $emptyStyle;
return $this;
}
- /**
- * @return OutputFormatterStyleInterface
- */
- public function getEmptyStyle()
+ public function getEmptyStyle(): OutputFormatterStyleInterface
{
return $this->emptyStyle;
}
diff --git a/Formatter/WrappableOutputFormatterInterface.php b/Formatter/WrappableOutputFormatterInterface.php
index 42319ee55..412d9976f 100644
--- a/Formatter/WrappableOutputFormatterInterface.php
+++ b/Formatter/WrappableOutputFormatterInterface.php
@@ -21,5 +21,5 @@ interface WrappableOutputFormatterInterface extends OutputFormatterInterface
/**
* Formats a message according to the given styles, wrapping at `$width` (0 means no wrapping).
*/
- public function formatAndWrap(?string $message, int $width);
+ public function formatAndWrap(?string $message, int $width): string;
}
diff --git a/Helper/DebugFormatterHelper.php b/Helper/DebugFormatterHelper.php
index e258ba050..dfdb8a82c 100644
--- a/Helper/DebugFormatterHelper.php
+++ b/Helper/DebugFormatterHelper.php
@@ -21,27 +21,23 @@
class DebugFormatterHelper extends Helper
{
private const COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'default'];
- private $started = [];
- private $count = -1;
+ private array $started = [];
+ private int $count = -1;
/**
* Starts a debug formatting session.
- *
- * @return string
*/
- public function start(string $id, string $message, string $prefix = 'RUN')
+ public function start(string $id, string $message, string $prefix = 'RUN'): string
{
$this->started[$id] = ['border' => ++$this->count % \count(self::COLORS)];
- return sprintf("%s %s > %s>\n", $this->getBorder($id), $prefix, $message);
+ return \sprintf("%s %s > %s>\n", $this->getBorder($id), $prefix, $message);
}
/**
* Adds progress to a formatting session.
- *
- * @return string
*/
- public function progress(string $id, string $buffer, bool $error = false, string $prefix = 'OUT', string $errorPrefix = 'ERR')
+ public function progress(string $id, string $buffer, bool $error = false, string $prefix = 'OUT', string $errorPrefix = 'ERR'): string
{
$message = '';
@@ -51,22 +47,22 @@ public function progress(string $id, string $buffer, bool $error = false, string
unset($this->started[$id]['out']);
}
if (!isset($this->started[$id]['err'])) {
- $message .= sprintf('%s %s > ', $this->getBorder($id), $errorPrefix);
+ $message .= \sprintf('%s %s > ', $this->getBorder($id), $errorPrefix);
$this->started[$id]['err'] = true;
}
- $message .= str_replace("\n", sprintf("\n%s %s > ", $this->getBorder($id), $errorPrefix), $buffer);
+ $message .= str_replace("\n", \sprintf("\n%s %s > ", $this->getBorder($id), $errorPrefix), $buffer);
} else {
if (isset($this->started[$id]['err'])) {
$message .= "\n";
unset($this->started[$id]['err']);
}
if (!isset($this->started[$id]['out'])) {
- $message .= sprintf('%s %s > ', $this->getBorder($id), $prefix);
+ $message .= \sprintf('%s %s > ', $this->getBorder($id), $prefix);
$this->started[$id]['out'] = true;
}
- $message .= str_replace("\n", sprintf("\n%s %s > ", $this->getBorder($id), $prefix), $buffer);
+ $message .= str_replace("\n", \sprintf("\n%s %s > ", $this->getBorder($id), $prefix), $buffer);
}
return $message;
@@ -74,18 +70,16 @@ public function progress(string $id, string $buffer, bool $error = false, string
/**
* Stops a formatting session.
- *
- * @return string
*/
- public function stop(string $id, string $message, bool $successful, string $prefix = 'RES')
+ public function stop(string $id, string $message, bool $successful, string $prefix = 'RES'): string
{
$trailingEOL = isset($this->started[$id]['out']) || isset($this->started[$id]['err']) ? "\n" : '';
if ($successful) {
- return sprintf("%s%s %s > %s>\n", $trailingEOL, $this->getBorder($id), $prefix, $message);
+ return \sprintf("%s%s %s > %s>\n", $trailingEOL, $this->getBorder($id), $prefix, $message);
}
- $message = sprintf("%s%s %s > %s>\n", $trailingEOL, $this->getBorder($id), $prefix, $message);
+ $message = \sprintf("%s%s %s > %s>\n", $trailingEOL, $this->getBorder($id), $prefix, $message);
unset($this->started[$id]['out'], $this->started[$id]['err']);
@@ -94,13 +88,10 @@ public function stop(string $id, string $message, bool $successful, string $pref
private function getBorder(string $id): string
{
- return sprintf(' >', self::COLORS[$this->started[$id]['border']]);
+ return \sprintf(' >', self::COLORS[$this->started[$id]['border']]);
}
- /**
- * {@inheritdoc}
- */
- public function getName()
+ public function getName(): string
{
return 'debug_formatter';
}
diff --git a/Helper/DescriptorHelper.php b/Helper/DescriptorHelper.php
index af85e9c0a..9422271fb 100644
--- a/Helper/DescriptorHelper.php
+++ b/Helper/DescriptorHelper.php
@@ -14,6 +14,7 @@
use Symfony\Component\Console\Descriptor\DescriptorInterface;
use Symfony\Component\Console\Descriptor\JsonDescriptor;
use Symfony\Component\Console\Descriptor\MarkdownDescriptor;
+use Symfony\Component\Console\Descriptor\ReStructuredTextDescriptor;
use Symfony\Component\Console\Descriptor\TextDescriptor;
use Symfony\Component\Console\Descriptor\XmlDescriptor;
use Symfony\Component\Console\Exception\InvalidArgumentException;
@@ -29,7 +30,7 @@ class DescriptorHelper extends Helper
/**
* @var DescriptorInterface[]
*/
- private $descriptors = [];
+ private array $descriptors = [];
public function __construct()
{
@@ -38,6 +39,7 @@ public function __construct()
->register('xml', new XmlDescriptor())
->register('json', new JsonDescriptor())
->register('md', new MarkdownDescriptor())
+ ->register('rst', new ReStructuredTextDescriptor())
;
}
@@ -50,7 +52,7 @@ public function __construct()
*
* @throws InvalidArgumentException when the given format is not supported
*/
- public function describe(OutputInterface $output, ?object $object, array $options = [])
+ public function describe(OutputInterface $output, ?object $object, array $options = []): void
{
$options = array_merge([
'raw_text' => false,
@@ -58,7 +60,7 @@ public function describe(OutputInterface $output, ?object $object, array $option
], $options);
if (!isset($this->descriptors[$options['format']])) {
- throw new InvalidArgumentException(sprintf('Unsupported format "%s".', $options['format']));
+ throw new InvalidArgumentException(\sprintf('Unsupported format "%s".', $options['format']));
}
$descriptor = $this->descriptors[$options['format']];
@@ -70,17 +72,14 @@ public function describe(OutputInterface $output, ?object $object, array $option
*
* @return $this
*/
- public function register(string $format, DescriptorInterface $descriptor)
+ public function register(string $format, DescriptorInterface $descriptor): static
{
$this->descriptors[$format] = $descriptor;
return $this;
}
- /**
- * {@inheritdoc}
- */
- public function getName()
+ public function getName(): string
{
return 'descriptor';
}
diff --git a/Helper/Dumper.php b/Helper/Dumper.php
index 605e4d70b..0cd01e616 100644
--- a/Helper/Dumper.php
+++ b/Helper/Dumper.php
@@ -21,43 +21,32 @@
*/
final class Dumper
{
- private $output;
- private $dumper;
- private $cloner;
- private $handler;
-
- public function __construct(OutputInterface $output, ?CliDumper $dumper = null, ?ClonerInterface $cloner = null)
- {
- $this->output = $output;
- $this->dumper = $dumper;
- $this->cloner = $cloner;
+ private \Closure $handler;
+ public function __construct(
+ private OutputInterface $output,
+ private ?CliDumper $dumper = null,
+ private ?ClonerInterface $cloner = null,
+ ) {
if (class_exists(CliDumper::class)) {
$this->handler = function ($var): string {
- $dumper = $this->dumper ?? $this->dumper = new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR);
+ $dumper = $this->dumper ??= new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR);
$dumper->setColors($this->output->isDecorated());
- return rtrim($dumper->dump(($this->cloner ?? $this->cloner = new VarCloner())->cloneVar($var)->withRefHandles(false), true));
+ return rtrim($dumper->dump(($this->cloner ??= new VarCloner())->cloneVar($var)->withRefHandles(false), true));
};
} else {
- $this->handler = function ($var): string {
- switch (true) {
- case null === $var:
- return 'null';
- case true === $var:
- return 'true';
- case false === $var:
- return 'false';
- case \is_string($var):
- return '"'.$var.'"';
- default:
- return rtrim(print_r($var, true));
- }
+ $this->handler = fn ($var): string => match (true) {
+ null === $var => 'null',
+ true === $var => 'true',
+ false === $var => 'false',
+ \is_string($var) => '"'.$var.'"',
+ default => rtrim(print_r($var, true)),
};
}
}
- public function __invoke($var): string
+ public function __invoke(mixed $var): string
{
return ($this->handler)($var);
}
diff --git a/Helper/FormatterHelper.php b/Helper/FormatterHelper.php
index 92d8dc724..3646b3d6f 100644
--- a/Helper/FormatterHelper.php
+++ b/Helper/FormatterHelper.php
@@ -22,22 +22,16 @@ class FormatterHelper extends Helper
{
/**
* Formats a message within a section.
- *
- * @return string
*/
- public function formatSection(string $section, string $message, string $style = 'info')
+ public function formatSection(string $section, string $message, string $style = 'info'): string
{
- return sprintf('<%s>[%s]%s> %s', $style, $section, $style, $message);
+ return \sprintf('<%s>[%s]%s> %s', $style, $section, $style, $message);
}
/**
* Formats a message as a block of text.
- *
- * @param string|array $messages The message to write in the block
- *
- * @return string
*/
- public function formatBlock($messages, string $style, bool $large = false)
+ public function formatBlock(string|array $messages, string $style, bool $large = false): string
{
if (!\is_array($messages)) {
$messages = [$messages];
@@ -47,7 +41,7 @@ public function formatBlock($messages, string $style, bool $large = false)
$lines = [];
foreach ($messages as $message) {
$message = OutputFormatter::escape($message);
- $lines[] = sprintf($large ? ' %s ' : ' %s ', $message);
+ $lines[] = \sprintf($large ? ' %s ' : ' %s ', $message);
$len = max(self::width($message) + ($large ? 4 : 2), $len);
}
@@ -60,7 +54,7 @@ public function formatBlock($messages, string $style, bool $large = false)
}
for ($i = 0; isset($messages[$i]); ++$i) {
- $messages[$i] = sprintf('<%s>%s%s>', $style, $messages[$i], $style);
+ $messages[$i] = \sprintf('<%s>%s%s>', $style, $messages[$i], $style);
}
return implode("\n", $messages);
@@ -68,10 +62,8 @@ public function formatBlock($messages, string $style, bool $large = false)
/**
* Truncates a message to the given length.
- *
- * @return string
*/
- public function truncate(string $message, int $length, string $suffix = '...')
+ public function truncate(string $message, int $length, string $suffix = '...'): string
{
$computedLength = $length - self::width($suffix);
@@ -82,10 +74,7 @@ public function truncate(string $message, int $length, string $suffix = '...')
return self::substr($message, 0, $length).$suffix;
}
- /**
- * {@inheritdoc}
- */
- public function getName()
+ public function getName(): string
{
return 'formatter';
}
diff --git a/Helper/Helper.php b/Helper/Helper.php
index 6b3f7f43a..3981bbf3a 100644
--- a/Helper/Helper.php
+++ b/Helper/Helper.php
@@ -21,45 +21,25 @@
*/
abstract class Helper implements HelperInterface
{
- protected $helperSet = null;
+ protected ?HelperSet $helperSet = null;
- /**
- * {@inheritdoc}
- */
- public function setHelperSet(?HelperSet $helperSet = null)
+ public function setHelperSet(?HelperSet $helperSet): void
{
$this->helperSet = $helperSet;
}
- /**
- * {@inheritdoc}
- */
- public function getHelperSet()
+ public function getHelperSet(): ?HelperSet
{
return $this->helperSet;
}
- /**
- * Returns the length of a string, using mb_strwidth if it is available.
- *
- * @deprecated since Symfony 5.3
- *
- * @return int
- */
- public static function strlen(?string $string)
- {
- trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::width() or Helper::length() instead.', __METHOD__);
-
- return self::width($string);
- }
-
/**
* Returns the width of a string, using mb_strwidth if it is available.
* The width is how many characters positions the string will use.
*/
public static function width(?string $string): int
{
- $string ?? $string = '';
+ $string ??= '';
if (preg_match('//u', $string)) {
return (new UnicodeString($string))->width(false);
@@ -78,7 +58,7 @@ public static function width(?string $string): int
*/
public static function length(?string $string): int
{
- $string ?? $string = '';
+ $string ??= '';
if (preg_match('//u', $string)) {
return (new UnicodeString($string))->length();
@@ -93,12 +73,10 @@ public static function length(?string $string): int
/**
* Returns the subset of a string, using mb_substr if it is available.
- *
- * @return string
*/
- public static function substr(?string $string, int $from, ?int $length = null)
+ public static function substr(?string $string, int $from, ?int $length = null): string
{
- $string ?? $string = '';
+ $string ??= '';
if (false === $encoding = mb_detect_encoding($string, null, true)) {
return substr($string, $from, $length);
@@ -107,63 +85,64 @@ public static function substr(?string $string, int $from, ?int $length = null)
return mb_substr($string, $from, $length, $encoding);
}
- public static function formatTime($secs)
+ public static function formatTime(int|float $secs, int $precision = 1): string
{
+ $secs = (int) floor($secs);
+
+ if (0 === $secs) {
+ return '< 1 sec';
+ }
+
static $timeFormats = [
- [0, '< 1 sec'],
- [1, '1 sec'],
- [2, 'secs', 1],
- [60, '1 min'],
- [120, 'mins', 60],
- [3600, '1 hr'],
- [7200, 'hrs', 3600],
- [86400, '1 day'],
- [172800, 'days', 86400],
+ [1, '1 sec', 'secs'],
+ [60, '1 min', 'mins'],
+ [3600, '1 hr', 'hrs'],
+ [86400, '1 day', 'days'],
];
+ $times = [];
foreach ($timeFormats as $index => $format) {
- if ($secs >= $format[0]) {
- if ((isset($timeFormats[$index + 1]) && $secs < $timeFormats[$index + 1][0])
- || $index == \count($timeFormats) - 1
- ) {
- if (2 == \count($format)) {
- return $format[1];
- }
-
- return floor($secs / $format[2]).' '.$format[1];
- }
+ $seconds = isset($timeFormats[$index + 1]) ? $secs % $timeFormats[$index + 1][0] : $secs;
+
+ if (isset($times[$index - $precision])) {
+ unset($times[$index - $precision]);
}
+
+ if (0 === $seconds) {
+ continue;
+ }
+
+ $unitCount = ($seconds / $format[0]);
+ $times[$index] = 1 === $unitCount ? $format[1] : $unitCount.' '.$format[2];
+
+ if ($secs === $seconds) {
+ break;
+ }
+
+ $secs -= $seconds;
}
+
+ return implode(', ', array_reverse($times));
}
- public static function formatMemory(int $memory)
+ public static function formatMemory(int $memory): string
{
if ($memory >= 1024 * 1024 * 1024) {
- return sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024);
+ return \sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024);
}
if ($memory >= 1024 * 1024) {
- return sprintf('%.1f MiB', $memory / 1024 / 1024);
+ return \sprintf('%.1f MiB', $memory / 1024 / 1024);
}
if ($memory >= 1024) {
- return sprintf('%d KiB', $memory / 1024);
+ return \sprintf('%d KiB', $memory / 1024);
}
- return sprintf('%d B', $memory);
- }
-
- /**
- * @deprecated since Symfony 5.3
- */
- public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, ?string $string)
- {
- trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::removeDecoration() instead.', __METHOD__);
-
- return self::width(self::removeDecoration($formatter, $string));
+ return \sprintf('%d B', $memory);
}
- public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string)
+ public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string): string
{
$isDecorated = $formatter->isDecorated();
$formatter->setDecorated(false);
diff --git a/Helper/HelperInterface.php b/Helper/HelperInterface.php
index 5bf4d6327..8c4da3c91 100644
--- a/Helper/HelperInterface.php
+++ b/Helper/HelperInterface.php
@@ -21,19 +21,15 @@ interface HelperInterface
/**
* Sets the helper set associated with this helper.
*/
- public function setHelperSet(?HelperSet $helperSet = null);
+ public function setHelperSet(?HelperSet $helperSet): void;
/**
* Gets the helper set associated with this helper.
- *
- * @return HelperSet|null
*/
- public function getHelperSet();
+ public function getHelperSet(): ?HelperSet;
/**
* Returns the canonical name of this helper.
- *
- * @return string
*/
- public function getName();
+ public function getName(): string;
}
diff --git a/Helper/HelperSet.php b/Helper/HelperSet.php
index c870ab997..ffe756c9d 100644
--- a/Helper/HelperSet.php
+++ b/Helper/HelperSet.php
@@ -11,7 +11,6 @@
namespace Symfony\Component\Console\Helper;
-use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
/**
@@ -19,16 +18,15 @@
*
* @author Fabien Potencier
*
- * @implements \IteratorAggregate
+ * @implements \IteratorAggregate
*/
class HelperSet implements \IteratorAggregate
{
- /** @var array */
- private $helpers = [];
- private $command;
+ /** @var array */
+ private array $helpers = [];
/**
- * @param Helper[] $helpers An array of helper
+ * @param HelperInterface[] $helpers
*/
public function __construct(array $helpers = [])
{
@@ -37,7 +35,7 @@ public function __construct(array $helpers = [])
}
}
- public function set(HelperInterface $helper, ?string $alias = null)
+ public function set(HelperInterface $helper, ?string $alias = null): void
{
$this->helpers[$helper->getName()] = $helper;
if (null !== $alias) {
@@ -49,10 +47,8 @@ public function set(HelperInterface $helper, ?string $alias = null)
/**
* Returns true if the helper if defined.
- *
- * @return bool
*/
- public function has(string $name)
+ public function has(string $name): bool
{
return isset($this->helpers[$name]);
}
@@ -60,48 +56,18 @@ public function has(string $name)
/**
* Gets a helper value.
*
- * @return HelperInterface
- *
* @throws InvalidArgumentException if the helper is not defined
*/
- public function get(string $name)
+ public function get(string $name): HelperInterface
{
if (!$this->has($name)) {
- throw new InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name));
+ throw new InvalidArgumentException(\sprintf('The helper "%s" is not defined.', $name));
}
return $this->helpers[$name];
}
- /**
- * @deprecated since Symfony 5.4
- */
- public function setCommand(?Command $command = null)
- {
- trigger_deprecation('symfony/console', '5.4', 'Method "%s()" is deprecated.', __METHOD__);
-
- $this->command = $command;
- }
-
- /**
- * Gets the command associated with this helper set.
- *
- * @return Command
- *
- * @deprecated since Symfony 5.4
- */
- public function getCommand()
- {
- trigger_deprecation('symfony/console', '5.4', 'Method "%s()" is deprecated.', __METHOD__);
-
- return $this->command;
- }
-
- /**
- * @return \Traversable
- */
- #[\ReturnTypeWillChange]
- public function getIterator()
+ public function getIterator(): \Traversable
{
return new \ArrayIterator($this->helpers);
}
diff --git a/Helper/InputAwareHelper.php b/Helper/InputAwareHelper.php
index 0d0dba23e..47126bdaa 100644
--- a/Helper/InputAwareHelper.php
+++ b/Helper/InputAwareHelper.php
@@ -21,12 +21,9 @@
*/
abstract class InputAwareHelper extends Helper implements InputAwareInterface
{
- protected $input;
+ protected InputInterface $input;
- /**
- * {@inheritdoc}
- */
- public function setInput(InputInterface $input)
+ public function setInput(InputInterface $input): void
{
$this->input = $input;
}
diff --git a/Helper/OutputWrapper.php b/Helper/OutputWrapper.php
new file mode 100644
index 000000000..a615ed2f9
--- /dev/null
+++ b/Helper/OutputWrapper.php
@@ -0,0 +1,76 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Helper;
+
+/**
+ * Simple output wrapper for "tagged outputs" instead of wordwrap(). This solution is based on a StackOverflow
+ * answer: https://stackoverflow.com/a/20434776/1476819 from user557597 (alias SLN).
+ *
+ * (?:
+ * # -- Words/Characters
+ * ( # (1 start)
+ * (?> # Atomic Group - Match words with valid breaks
+ * .{1,16} # 1-N characters
+ * # Followed by one of 4 prioritized, non-linebreak whitespace
+ * (?: # break types:
+ * (?<= [^\S\r\n] ) # 1. - Behind a non-linebreak whitespace
+ * [^\S\r\n]? # ( optionally accept an extra non-linebreak whitespace )
+ * | (?= \r? \n ) # 2. - Ahead a linebreak
+ * | $ # 3. - EOS
+ * | [^\S\r\n] # 4. - Accept an extra non-linebreak whitespace
+ * )
+ * ) # End atomic group
+ * |
+ * .{1,16} # No valid word breaks, just break on the N'th character
+ * ) # (1 end)
+ * (?: \r? \n )? # Optional linebreak after Words/Characters
+ * |
+ * # -- Or, Linebreak
+ * (?: \r? \n | $ ) # Stand alone linebreak or at EOS
+ * )
+ *
+ * @author Krisztián Ferenczi
+ *
+ * @see https://stackoverflow.com/a/20434776/1476819
+ */
+final class OutputWrapper
+{
+ private const TAG_OPEN_REGEX_SEGMENT = '[a-z](?:[^\\\\<>]*+ | \\\\.)*';
+ private const TAG_CLOSE_REGEX_SEGMENT = '[a-z][^<>]*+';
+ private const URL_PATTERN = 'https?://\S+';
+
+ public function __construct(
+ private bool $allowCutUrls = false,
+ ) {
+ }
+
+ public function wrap(string $text, int $width, string $break = "\n"): string
+ {
+ if (!$width) {
+ return $text;
+ }
+
+ $tagPattern = \sprintf('<(?:(?:%s)|/(?:%s)?)>', self::TAG_OPEN_REGEX_SEGMENT, self::TAG_CLOSE_REGEX_SEGMENT);
+ $limitPattern = "{1,$width}";
+ $patternBlocks = [$tagPattern];
+ if (!$this->allowCutUrls) {
+ $patternBlocks[] = self::URL_PATTERN;
+ }
+ $patternBlocks[] = '.';
+ $blocks = implode('|', $patternBlocks);
+ $rowPattern = "(?:$blocks)$limitPattern";
+ $pattern = \sprintf('#(?:((?>(%1$s)((?<=[^\S\r\n])[^\S\r\n]?|(?=\r?\n)|$|[^\S\r\n]))|(%1$s))(?:\r?\n)?|(?:\r?\n|$))#imux', $rowPattern);
+ $output = rtrim(preg_replace($pattern, '\\1'.$break, $text), $break);
+
+ return str_replace(' '.$break, $break, $output);
+ }
+}
diff --git a/Helper/ProcessHelper.php b/Helper/ProcessHelper.php
index 86a250b27..4a8cfc9d9 100644
--- a/Helper/ProcessHelper.php
+++ b/Helper/ProcessHelper.php
@@ -32,7 +32,7 @@ class ProcessHelper extends Helper
* @param callable|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
*/
- public function run(OutputInterface $output, $cmd, ?string $error = null, ?callable $callback = null, int $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE): Process
+ public function run(OutputInterface $output, array|Process $cmd, ?string $error = null, ?callable $callback = null, int $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE): Process
{
if (!class_exists(Process::class)) {
throw new \LogicException('The ProcessHelper cannot be run as the Process component is not installed. Try running "compose require symfony/process".');
@@ -48,10 +48,6 @@ public function run(OutputInterface $output, $cmd, ?string $error = null, ?calla
$cmd = [$cmd];
}
- if (!\is_array($cmd)) {
- throw new \TypeError(sprintf('The "command" argument of "%s()" must be an array or a "%s" instance, "%s" given.', __METHOD__, Process::class, get_debug_type($cmd)));
- }
-
if (\is_string($cmd[0] ?? null)) {
$process = new Process($cmd);
$cmd = [];
@@ -59,7 +55,7 @@ public function run(OutputInterface $output, $cmd, ?string $error = null, ?calla
$process = $cmd[0];
unset($cmd[0]);
} else {
- throw new \InvalidArgumentException(sprintf('Invalid command provided to "%s()": the command should be an array whose first element is either the path to the binary to run or a "Process" object.', __METHOD__));
+ throw new \InvalidArgumentException(\sprintf('Invalid command provided to "%s()": the command should be an array whose first element is either the path to the binary to run or a "Process" object.', __METHOD__));
}
if ($verbosity <= $output->getVerbosity()) {
@@ -73,12 +69,12 @@ public function run(OutputInterface $output, $cmd, ?string $error = null, ?calla
$process->run($callback, $cmd);
if ($verbosity <= $output->getVerbosity()) {
- $message = $process->isSuccessful() ? 'Command ran successfully' : sprintf('%s Command did not run successfully', $process->getExitCode());
+ $message = $process->isSuccessful() ? 'Command ran successfully' : \sprintf('%s Command did not run successfully', $process->getExitCode());
$output->write($formatter->stop(spl_object_hash($process), $message, $process->isSuccessful()));
}
if (!$process->isSuccessful() && null !== $error) {
- $output->writeln(sprintf('%s', $this->escapeString($error)));
+ $output->writeln(\sprintf('%s', $this->escapeString($error)));
}
return $process;
@@ -98,9 +94,9 @@ public function run(OutputInterface $output, $cmd, ?string $error = null, ?calla
*
* @see run()
*/
- public function mustRun(OutputInterface $output, $cmd, ?string $error = null, ?callable $callback = null): Process
+ public function mustRun(OutputInterface $output, array|Process $cmd, ?string $error = null, ?callable $callback = null, int $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE): Process
{
- $process = $this->run($output, $cmd, $error, $callback);
+ $process = $this->run($output, $cmd, $error, $callback, $verbosity);
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
@@ -134,9 +130,6 @@ private function escapeString(string $str): string
return str_replace('<', '\\<', $str);
}
- /**
- * {@inheritdoc}
- */
public function getName(): string
{
return 'process';
diff --git a/Helper/ProgressBar.php b/Helper/ProgressBar.php
index 6250732eb..dc3605ad2 100644
--- a/Helper/ProgressBar.php
+++ b/Helper/ProgressBar.php
@@ -36,31 +36,33 @@ final class ProgressBar
private const FORMAT_DEBUG_NOMAX = 'debug_nomax';
private const FORMAT_NORMAL_NOMAX = 'normal_nomax';
- private $barWidth = 28;
- private $barChar;
- private $emptyBarChar = '-';
- private $progressChar = '>';
- private $format;
- private $internalFormat;
- private $redrawFreq = 1;
- private $writeCount;
- private $lastWriteTime;
- private $minSecondsBetweenRedraws = 0;
- private $maxSecondsBetweenRedraws = 1;
- private $output;
- private $step = 0;
- private $max;
- private $startTime;
- private $stepWidth;
- private $percent = 0.0;
- private $messages = [];
- private $overwrite = true;
- private $terminal;
- private $previousMessage;
- private $cursor;
-
- private static $formatters;
- private static $formats;
+ private int $barWidth = 28;
+ private string $barChar;
+ private string $emptyBarChar = '-';
+ private string $progressChar = '>';
+ private ?string $format = null;
+ private ?string $internalFormat = null;
+ private ?int $redrawFreq = 1;
+ private int $writeCount = 0;
+ private float $lastWriteTime = 0;
+ private float $minSecondsBetweenRedraws = 0;
+ private float $maxSecondsBetweenRedraws = 1;
+ private OutputInterface $output;
+ private int $step = 0;
+ private int $startingStep = 0;
+ private ?int $max = null;
+ private int $startTime;
+ private int $stepWidth;
+ private float $percent = 0.0;
+ private array $messages = [];
+ private bool $overwrite = true;
+ private Terminal $terminal;
+ private ?string $previousMessage = null;
+ private Cursor $cursor;
+ private array $placeholders = [];
+
+ private static array $formatters;
+ private static array $formats;
/**
* @param int $max Maximum steps (0 if unknown)
@@ -93,18 +95,16 @@ public function __construct(OutputInterface $output, int $max = 0, float $minSec
}
/**
- * Sets a placeholder formatter for a given name.
+ * Sets a placeholder formatter for a given name, globally for all instances of ProgressBar.
*
* This method also allow you to override an existing placeholder.
*
- * @param string $name The placeholder name (including the delimiter char like %)
- * @param callable $callable A PHP callable
+ * @param string $name The placeholder name (including the delimiter char like %)
+ * @param callable(ProgressBar):string $callable A PHP callable
*/
public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void
{
- if (!self::$formatters) {
- self::$formatters = self::initPlaceholderFormatters();
- }
+ self::$formatters ??= self::initPlaceholderFormatters();
self::$formatters[$name] = $callable;
}
@@ -116,13 +116,31 @@ public static function setPlaceholderFormatterDefinition(string $name, callable
*/
public static function getPlaceholderFormatterDefinition(string $name): ?callable
{
- if (!self::$formatters) {
- self::$formatters = self::initPlaceholderFormatters();
- }
+ self::$formatters ??= self::initPlaceholderFormatters();
return self::$formatters[$name] ?? null;
}
+ /**
+ * Sets a placeholder formatter for a given name, for this instance only.
+ *
+ * @param callable(ProgressBar):string $callable A PHP callable
+ */
+ public function setPlaceholderFormatter(string $name, callable $callable): void
+ {
+ $this->placeholders[$name] = $callable;
+ }
+
+ /**
+ * Gets the placeholder formatter for a given name.
+ *
+ * @param string $name The placeholder name (including the delimiter char like %)
+ */
+ public function getPlaceholderFormatter(string $name): ?callable
+ {
+ return $this->placeholders[$name] ?? $this::getPlaceholderFormatterDefinition($name);
+ }
+
/**
* Sets a format for a given name.
*
@@ -133,9 +151,7 @@ public static function getPlaceholderFormatterDefinition(string $name): ?callabl
*/
public static function setFormatDefinition(string $name, string $format): void
{
- if (!self::$formats) {
- self::$formats = self::initFormats();
- }
+ self::$formats ??= self::initFormats();
self::$formats[$name] = $format;
}
@@ -147,9 +163,7 @@ public static function setFormatDefinition(string $name, string $format): void
*/
public static function getFormatDefinition(string $name): ?string
{
- if (!self::$formats) {
- self::$formats = self::initFormats();
- }
+ self::$formats ??= self::initFormats();
return self::$formats[$name] ?? null;
}
@@ -164,15 +178,12 @@ public static function getFormatDefinition(string $name): ?string
* @param string $message The text to associate with the placeholder
* @param string $name The name of the placeholder
*/
- public function setMessage(string $message, string $name = 'message')
+ public function setMessage(string $message, string $name = 'message'): void
{
$this->messages[$name] = $message;
}
- /**
- * @return string|null
- */
- public function getMessage(string $name = 'message')
+ public function getMessage(string $name = 'message'): ?string
{
return $this->messages[$name] ?? null;
}
@@ -184,7 +195,7 @@ public function getStartTime(): int
public function getMaxSteps(): int
{
- return $this->max;
+ return $this->max ?? 0;
}
public function getProgress(): int
@@ -204,28 +215,28 @@ public function getProgressPercent(): float
public function getBarOffset(): float
{
- return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth);
+ return floor(null !== $this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth);
}
public function getEstimated(): float
{
- if (!$this->step) {
+ if (0 === $this->step || $this->step === $this->startingStep) {
return 0;
}
- return round((time() - $this->startTime) / $this->step * $this->max);
+ return round((time() - $this->startTime) / ($this->step - $this->startingStep) * $this->max);
}
public function getRemaining(): float
{
- if (!$this->step) {
+ if (0 === $this->step || $this->step === $this->startingStep) {
return 0;
}
- return round((time() - $this->startTime) / $this->step * ($this->max - $this->step));
+ return round((time() - $this->startTime) / ($this->step - $this->startingStep) * ($this->max - $this->step));
}
- public function setBarWidth(int $size)
+ public function setBarWidth(int $size): void
{
$this->barWidth = max(1, $size);
}
@@ -235,17 +246,17 @@ public function getBarWidth(): int
return $this->barWidth;
}
- public function setBarCharacter(string $char)
+ public function setBarCharacter(string $char): void
{
$this->barChar = $char;
}
public function getBarCharacter(): string
{
- return $this->barChar ?? ($this->max ? '=' : $this->emptyBarChar);
+ return $this->barChar ?? (null !== $this->max ? '=' : $this->emptyBarChar);
}
- public function setEmptyBarCharacter(string $char)
+ public function setEmptyBarCharacter(string $char): void
{
$this->emptyBarChar = $char;
}
@@ -255,7 +266,7 @@ public function getEmptyBarCharacter(): string
return $this->emptyBarChar;
}
- public function setProgressCharacter(string $char)
+ public function setProgressCharacter(string $char): void
{
$this->progressChar = $char;
}
@@ -265,7 +276,7 @@ public function getProgressCharacter(): string
return $this->progressChar;
}
- public function setFormat(string $format)
+ public function setFormat(string $format): void
{
$this->format = null;
$this->internalFormat = $format;
@@ -276,7 +287,7 @@ public function setFormat(string $format)
*
* @param int|null $freq The frequency in steps
*/
- public function setRedrawFrequency(?int $freq)
+ public function setRedrawFrequency(?int $freq): void
{
$this->redrawFreq = null !== $freq ? max(1, $freq) : null;
}
@@ -294,11 +305,31 @@ public function maxSecondsBetweenRedraws(float $seconds): void
/**
* Returns an iterator that will automatically update the progress bar when iterated.
*
- * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
+ * @template TKey
+ * @template TValue
+ *
+ * @param iterable $iterable
+ * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
+ *
+ * @return iterable
*/
public function iterate(iterable $iterable, ?int $max = null): iterable
{
- $this->start($max ?? (is_countable($iterable) ? \count($iterable) : 0));
+ if (0 === $max) {
+ $max = null;
+ }
+
+ $max ??= is_countable($iterable) ? \count($iterable) : null;
+
+ if (0 === $max) {
+ $this->max = 0;
+ $this->stepWidth = 2;
+ $this->finish();
+
+ return;
+ }
+
+ $this->start($max);
foreach ($iterable as $key => $value) {
yield $key => $value;
@@ -312,13 +343,16 @@ public function iterate(iterable $iterable, ?int $max = null): iterable
/**
* Starts the progress output.
*
- * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged
+ * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged
+ * @param int $startAt The starting point of the bar (useful e.g. when resuming a previously started bar)
*/
- public function start(?int $max = null)
+ public function start(?int $max = null, int $startAt = 0): void
{
$this->startTime = time();
- $this->step = 0;
- $this->percent = 0.0;
+ $this->step = $startAt;
+ $this->startingStep = $startAt;
+
+ $startAt > 0 ? $this->setProgress($startAt) : $this->percent = 0.0;
if (null !== $max) {
$this->setMaxSteps($max);
@@ -332,7 +366,7 @@ public function start(?int $max = null)
*
* @param int $step Number of steps to advance
*/
- public function advance(int $step = 1)
+ public function advance(int $step = 1): void
{
$this->setProgress($this->step + $step);
}
@@ -340,12 +374,12 @@ public function advance(int $step = 1)
/**
* Sets whether to overwrite the progressbar, false for new line.
*/
- public function setOverwrite(bool $overwrite)
+ public function setOverwrite(bool $overwrite): void
{
$this->overwrite = $overwrite;
}
- public function setProgress(int $step)
+ public function setProgress(int $step): void
{
if ($this->max && $step > $this->max) {
$this->max = $step;
@@ -353,11 +387,15 @@ public function setProgress(int $step)
$step = 0;
}
- $redrawFreq = $this->redrawFreq ?? (($this->max ?: 10) / 10);
- $prevPeriod = (int) ($this->step / $redrawFreq);
- $currPeriod = (int) ($step / $redrawFreq);
+ $redrawFreq = $this->redrawFreq ?? (($this->max ?? 10) / 10);
+ $prevPeriod = $redrawFreq ? (int) ($this->step / $redrawFreq) : 0;
+ $currPeriod = $redrawFreq ? (int) ($step / $redrawFreq) : 0;
$this->step = $step;
- $this->percent = $this->max ? (float) $this->step / $this->max : 0;
+ $this->percent = match ($this->max) {
+ null => 0,
+ 0 => 1,
+ default => (float) $this->step / $this->max,
+ };
$timeInterval = microtime(true) - $this->lastWriteTime;
// Draw regardless of other limits
@@ -378,11 +416,20 @@ public function setProgress(int $step)
}
}
- public function setMaxSteps(int $max)
+ public function setMaxSteps(?int $max): void
{
+ if (0 === $max) {
+ $max = null;
+ }
+
$this->format = null;
- $this->max = max(0, $max);
- $this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4;
+ if (null === $max) {
+ $this->max = null;
+ $this->stepWidth = 4;
+ } else {
+ $this->max = max(0, $max);
+ $this->stepWidth = Helper::width((string) $this->max);
+ }
}
/**
@@ -390,16 +437,16 @@ public function setMaxSteps(int $max)
*/
public function finish(): void
{
- if (!$this->max) {
+ if (null === $this->max) {
$this->max = $this->step;
}
- if ($this->step === $this->max && !$this->overwrite) {
+ if (($this->step === $this->max || null === $this->max) && !$this->overwrite) {
// prevent double 100% output
return;
}
- $this->setProgress($this->max);
+ $this->setProgress($this->max ?? $this->step);
}
/**
@@ -438,7 +485,7 @@ public function clear(): void
$this->overwrite('');
}
- private function setRealFormat(string $format)
+ private function setRealFormat(string $format): void
{
// try to use the _nomax variant if available
if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) {
@@ -466,12 +513,21 @@ private function overwrite(string $message): void
if ($this->output instanceof ConsoleSectionOutput) {
$messageLines = explode("\n", $this->previousMessage);
$lineCount = \count($messageLines);
+
+ $lastLineWithoutDecoration = Helper::removeDecoration($this->output->getFormatter(), end($messageLines) ?? '');
+
+ // When the last previous line is empty (without formatting) it is already cleared by the section output, so we don't need to clear it again
+ if ('' === $lastLineWithoutDecoration) {
+ --$lineCount;
+ }
+
foreach ($messageLines as $messageLine) {
$messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine));
if ($messageLineLength > $this->terminal->getWidth()) {
$lineCount += floor($messageLineLength / $this->terminal->getWidth());
}
}
+
$this->output->clear($lineCount);
} else {
$lineCount = substr_count($this->previousMessage, "\n");
@@ -498,17 +554,13 @@ private function overwrite(string $message): void
private function determineBestFormat(): string
{
- switch ($this->output->getVerbosity()) {
+ return match ($this->output->getVerbosity()) {
// OutputInterface::VERBOSITY_QUIET: display is disabled anyway
- case OutputInterface::VERBOSITY_VERBOSE:
- return $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX;
- case OutputInterface::VERBOSITY_VERY_VERBOSE:
- return $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX;
- case OutputInterface::VERBOSITY_DEBUG:
- return $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX;
- default:
- return $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX;
- }
+ OutputInterface::VERBOSITY_VERBOSE => $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX,
+ OutputInterface::VERBOSITY_VERY_VERBOSE => $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX,
+ OutputInterface::VERBOSITY_DEBUG => $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX,
+ default => $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX,
+ };
}
private static function initPlaceholderFormatters(): array
@@ -524,35 +576,25 @@ private static function initPlaceholderFormatters(): array
return $display;
},
- 'elapsed' => function (self $bar) {
- return Helper::formatTime(time() - $bar->getStartTime());
- },
+ 'elapsed' => fn (self $bar) => Helper::formatTime(time() - $bar->getStartTime(), 2),
'remaining' => function (self $bar) {
- if (!$bar->getMaxSteps()) {
+ if (null === $bar->getMaxSteps()) {
throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.');
}
- return Helper::formatTime($bar->getRemaining());
+ return Helper::formatTime($bar->getRemaining(), 2);
},
'estimated' => function (self $bar) {
- if (!$bar->getMaxSteps()) {
+ if (null === $bar->getMaxSteps()) {
throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.');
}
- return Helper::formatTime($bar->getEstimated());
- },
- 'memory' => function (self $bar) {
- return Helper::formatMemory(memory_get_usage(true));
- },
- 'current' => function (self $bar) {
- return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT);
- },
- 'max' => function (self $bar) {
- return $bar->getMaxSteps();
- },
- 'percent' => function (self $bar) {
- return floor($bar->getProgressPercent() * 100);
+ return Helper::formatTime($bar->getEstimated(), 2);
},
+ 'memory' => fn (self $bar) => Helper::formatMemory(memory_get_usage(true)),
+ 'current' => fn (self $bar) => str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT),
+ 'max' => fn (self $bar) => $bar->getMaxSteps(),
+ 'percent' => fn (self $bar) => floor($bar->getProgressPercent() * 100),
];
}
@@ -575,9 +617,11 @@ private static function initFormats(): array
private function buildLine(): string
{
- $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i";
+ \assert(null !== $this->format);
+
+ $regex = '{%([a-z\-_]+)(?:\:([^%]+))?%}i';
$callback = function ($matches) {
- if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) {
+ if ($formatter = $this->getPlaceholderFormatter($matches[1])) {
$text = $formatter($this, $this->output);
} elseif (isset($this->messages[$matches[1]])) {
$text = $this->messages[$matches[1]];
@@ -586,7 +630,7 @@ private function buildLine(): string
}
if (isset($matches[2])) {
- $text = sprintf('%'.$matches[2], $text);
+ $text = \sprintf('%'.$matches[2], $text);
}
return $text;
@@ -594,9 +638,7 @@ private function buildLine(): string
$line = preg_replace_callback($regex, $callback, $this->format);
// gets string length for each sub line with multiline format
- $linesLength = array_map(function ($subLine) {
- return Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r")));
- }, explode("\n", $line));
+ $linesLength = array_map(fn ($subLine) => Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r"))), explode("\n", $line));
$linesWidth = max($linesLength);
diff --git a/Helper/ProgressIndicator.php b/Helper/ProgressIndicator.php
index 3cc0e1451..b6bbd0cfa 100644
--- a/Helper/ProgressIndicator.php
+++ b/Helper/ProgressIndicator.php
@@ -31,53 +31,51 @@ class ProgressIndicator
'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)',
];
- private $output;
- private $startTime;
- private $format;
- private $message;
- private $indicatorValues;
- private $indicatorCurrent;
- private $indicatorChangeInterval;
- private $indicatorUpdateTime;
- private $started = false;
+ private int $startTime;
+ private ?string $format = null;
+ private ?string $message = null;
+ private array $indicatorValues;
+ private int $indicatorCurrent;
+ private string $finishedIndicatorValue;
+ private float $indicatorUpdateTime;
+ private bool $started = false;
+ private bool $finished = false;
/**
* @var array
*/
- private static $formatters;
+ private static array $formatters;
/**
* @param int $indicatorChangeInterval Change interval in milliseconds
* @param array|null $indicatorValues Animated indicator characters
*/
- public function __construct(OutputInterface $output, ?string $format = null, int $indicatorChangeInterval = 100, ?array $indicatorValues = null)
- {
- $this->output = $output;
-
- if (null === $format) {
- $format = $this->determineBestFormat();
- }
-
- if (null === $indicatorValues) {
- $indicatorValues = ['-', '\\', '|', '/'];
- }
-
+ public function __construct(
+ private OutputInterface $output,
+ ?string $format = null,
+ private int $indicatorChangeInterval = 100,
+ ?array $indicatorValues = null,
+ ?string $finishedIndicatorValue = null,
+ ) {
+ $format ??= $this->determineBestFormat();
+ $indicatorValues ??= ['-', '\\', '|', '/'];
$indicatorValues = array_values($indicatorValues);
+ $finishedIndicatorValue ??= '✔';
if (2 > \count($indicatorValues)) {
throw new InvalidArgumentException('Must have at least 2 indicator value characters.');
}
$this->format = self::getFormatDefinition($format);
- $this->indicatorChangeInterval = $indicatorChangeInterval;
$this->indicatorValues = $indicatorValues;
+ $this->finishedIndicatorValue = $finishedIndicatorValue;
$this->startTime = time();
}
/**
* Sets the current indicator message.
*/
- public function setMessage(?string $message)
+ public function setMessage(?string $message): void
{
$this->message = $message;
@@ -87,7 +85,7 @@ public function setMessage(?string $message)
/**
* Starts the indicator output.
*/
- public function start(string $message)
+ public function start(string $message): void
{
if ($this->started) {
throw new LogicException('Progress indicator already started.');
@@ -95,6 +93,7 @@ public function start(string $message)
$this->message = $message;
$this->started = true;
+ $this->finished = false;
$this->startTime = time();
$this->indicatorUpdateTime = $this->getCurrentTimeInMilliseconds() + $this->indicatorChangeInterval;
$this->indicatorCurrent = 0;
@@ -105,7 +104,7 @@ public function start(string $message)
/**
* Advances the indicator.
*/
- public function advance()
+ public function advance(): void
{
if (!$this->started) {
throw new LogicException('Progress indicator has not yet been started.');
@@ -129,13 +128,25 @@ public function advance()
/**
* Finish the indicator with message.
+ *
+ * @param ?string $finishedIndicator
*/
- public function finish(string $message)
+ public function finish(string $message/* , ?string $finishedIndicator = null */): void
{
+ $finishedIndicator = 1 < \func_num_args() ? func_get_arg(1) : null;
+ if (null !== $finishedIndicator && !\is_string($finishedIndicator)) {
+ throw new \TypeError(\sprintf('Argument 2 passed to "%s()" must be of the type string or null, "%s" given.', __METHOD__, get_debug_type($finishedIndicator)));
+ }
+
if (!$this->started) {
throw new LogicException('Progress indicator has not yet been started.');
}
+ if (null !== $finishedIndicator) {
+ $this->finishedIndicatorValue = $finishedIndicator;
+ }
+
+ $this->finished = true;
$this->message = $message;
$this->display();
$this->output->writeln('');
@@ -144,10 +155,8 @@ public function finish(string $message)
/**
* Gets the format for a given name.
- *
- * @return string|null
*/
- public static function getFormatDefinition(string $name)
+ public static function getFormatDefinition(string $name): ?string
{
return self::FORMATS[$name] ?? null;
}
@@ -157,36 +166,30 @@ public static function getFormatDefinition(string $name)
*
* This method also allow you to override an existing placeholder.
*/
- public static function setPlaceholderFormatterDefinition(string $name, callable $callable)
+ public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void
{
- if (!self::$formatters) {
- self::$formatters = self::initPlaceholderFormatters();
- }
+ self::$formatters ??= self::initPlaceholderFormatters();
self::$formatters[$name] = $callable;
}
/**
* Gets the placeholder formatter for a given name (including the delimiter char like %).
- *
- * @return callable|null
*/
- public static function getPlaceholderFormatterDefinition(string $name)
+ public static function getPlaceholderFormatterDefinition(string $name): ?callable
{
- if (!self::$formatters) {
- self::$formatters = self::initPlaceholderFormatters();
- }
+ self::$formatters ??= self::initPlaceholderFormatters();
return self::$formatters[$name] ?? null;
}
- private function display()
+ private function display(): void
{
if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) {
return;
}
- $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) {
+ $this->overwrite(preg_replace_callback('{%([a-z\-_]+)(?:\:([^%]+))?%}i', function ($matches) {
if ($formatter = self::getPlaceholderFormatterDefinition($matches[1])) {
return $formatter($this);
}
@@ -197,22 +200,19 @@ private function display()
private function determineBestFormat(): string
{
- switch ($this->output->getVerbosity()) {
+ return match ($this->output->getVerbosity()) {
// OutputInterface::VERBOSITY_QUIET: display is disabled anyway
- case OutputInterface::VERBOSITY_VERBOSE:
- return $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi';
- case OutputInterface::VERBOSITY_VERY_VERBOSE:
- case OutputInterface::VERBOSITY_DEBUG:
- return $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi';
- default:
- return $this->output->isDecorated() ? 'normal' : 'normal_no_ansi';
- }
+ OutputInterface::VERBOSITY_VERBOSE => $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi',
+ OutputInterface::VERBOSITY_VERY_VERBOSE,
+ OutputInterface::VERBOSITY_DEBUG => $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi',
+ default => $this->output->isDecorated() ? 'normal' : 'normal_no_ansi',
+ };
}
/**
* Overwrites a previous message to the output.
*/
- private function overwrite(string $message)
+ private function overwrite(string $message): void
{
if ($this->output->isDecorated()) {
$this->output->write("\x0D\x1B[2K");
@@ -227,21 +227,16 @@ private function getCurrentTimeInMilliseconds(): float
return round(microtime(true) * 1000);
}
+ /**
+ * @return array
+ */
private static function initPlaceholderFormatters(): array
{
return [
- 'indicator' => function (self $indicator) {
- return $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)];
- },
- 'message' => function (self $indicator) {
- return $indicator->message;
- },
- 'elapsed' => function (self $indicator) {
- return Helper::formatTime(time() - $indicator->startTime);
- },
- 'memory' => function () {
- return Helper::formatMemory(memory_get_usage(true));
- },
+ 'indicator' => fn (self $indicator) => $indicator->finished ? $indicator->finishedIndicatorValue : $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)],
+ 'message' => fn (self $indicator) => $indicator->message,
+ 'elapsed' => fn (self $indicator) => Helper::formatTime(time() - $indicator->startTime, 2),
+ 'memory' => fn () => Helper::formatMemory(memory_get_usage(true)),
];
}
}
diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php
index 7b9de9229..69afc2a67 100644
--- a/Helper/QuestionHelper.php
+++ b/Helper/QuestionHelper.php
@@ -34,13 +34,8 @@
*/
class QuestionHelper extends Helper
{
- /**
- * @var resource|null
- */
- private $inputStream;
-
- private static $stty = true;
- private static $stdinIsInteractive;
+ private static bool $stty = true;
+ private static bool $stdinIsInteractive;
/**
* Asks a question to the user.
@@ -49,7 +44,7 @@ class QuestionHelper extends Helper
*
* @throws RuntimeException If there is no data to read in the input stream
*/
- public function ask(InputInterface $input, OutputInterface $output, Question $question)
+ public function ask(InputInterface $input, OutputInterface $output, Question $question): mixed
{
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
@@ -59,18 +54,15 @@ public function ask(InputInterface $input, OutputInterface $output, Question $qu
return $this->getDefaultAnswer($question);
}
- if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) {
- $this->inputStream = $stream;
- }
+ $inputStream = $input instanceof StreamableInputInterface ? $input->getStream() : null;
+ $inputStream ??= STDIN;
try {
if (!$question->getValidator()) {
- return $this->doAsk($output, $question);
+ return $this->doAsk($inputStream, $output, $question);
}
- $interviewer = function () use ($output, $question) {
- return $this->doAsk($output, $question);
- };
+ $interviewer = fn () => $this->doAsk($inputStream, $output, $question);
return $this->validateAttempts($interviewer, $output, $question);
} catch (MissingInputException $exception) {
@@ -84,10 +76,7 @@ public function ask(InputInterface $input, OutputInterface $output, Question $qu
}
}
- /**
- * {@inheritdoc}
- */
- public function getName()
+ public function getName(): string
{
return 'question';
}
@@ -95,7 +84,7 @@ public function getName()
/**
* Prevents usage of stty.
*/
- public static function disableStty()
+ public static function disableStty(): void
{
self::$stty = false;
}
@@ -103,15 +92,14 @@ public static function disableStty()
/**
* Asks the question to the user.
*
- * @return mixed
+ * @param resource $inputStream
*
* @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
*/
- private function doAsk(OutputInterface $output, Question $question)
+ private function doAsk($inputStream, OutputInterface $output, Question $question): mixed
{
$this->writePrompt($output, $question);
- $inputStream = $this->inputStream ?: \STDIN;
$autocomplete = $question->getAutocompleterCallback();
if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) {
@@ -153,6 +141,7 @@ private function doAsk(OutputInterface $output, Question $question)
}
if ($output instanceof ConsoleSectionOutput) {
+ $output->addContent(''); // add EOL to the question
$output->addContent($ret);
}
@@ -165,10 +154,7 @@ private function doAsk(OutputInterface $output, Question $question)
return $ret;
}
- /**
- * @return mixed
- */
- private function getDefaultAnswer(Question $question)
+ private function getDefaultAnswer(Question $question): mixed
{
$default = $question->getDefault();
@@ -177,7 +163,7 @@ private function getDefaultAnswer(Question $question)
}
if ($validator = $question->getValidator()) {
- return \call_user_func($question->getValidator(), $default);
+ return \call_user_func($validator, $default);
} elseif ($question instanceof ChoiceQuestion) {
$choices = $question->getChoices();
@@ -198,7 +184,7 @@ private function getDefaultAnswer(Question $question)
/**
* Outputs the question prompt.
*/
- protected function writePrompt(OutputInterface $output, Question $question)
+ protected function writePrompt(OutputInterface $output, Question $question): void
{
$message = $question->getQuestion();
@@ -216,7 +202,7 @@ protected function writePrompt(OutputInterface $output, Question $question)
/**
* @return string[]
*/
- protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag)
+ protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag): array
{
$messages = [];
@@ -225,7 +211,7 @@ protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string
foreach ($choices as $key => $value) {
$padding = str_repeat(' ', $maxWidth - self::width($key));
- $messages[] = sprintf(" [<$tag>%s$padding$tag>] %s", $key, $value);
+ $messages[] = \sprintf(" [<$tag>%s$padding$tag>] %s", $key, $value);
}
return $messages;
@@ -234,7 +220,7 @@ protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string
/**
* Outputs an error message.
*/
- protected function writeError(OutputInterface $output, \Exception $error)
+ protected function writeError(OutputInterface $output, \Exception $error): void
{
if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) {
$message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error');
@@ -332,9 +318,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu
$matches = array_filter(
$autocomplete($ret),
- function ($match) use ($ret) {
- return '' === $ret || str_starts_with($match, $ret);
- }
+ fn ($match) => '' === $ret || str_starts_with($match, $ret)
);
$numMatches = \count($matches);
$ofs = -1;
@@ -422,7 +406,7 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $
$exe = __DIR__.'/../Resources/bin/hiddeninput.exe';
// handle code running from a phar
- if ('phar:' === substr(__FILE__, 0, 5)) {
+ if (str_starts_with(__FILE__, 'phar:')) {
$tmpExe = sys_get_temp_dir().'/hiddeninput.exe';
copy($exe, $tmpExe);
$exe = $tmpExe;
@@ -448,6 +432,11 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $
$value = fgets($inputStream, 4096);
+ if (4095 === \strlen($value)) {
+ $errOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output;
+ $errOutput->warning('The value was possibly truncated by your shell or terminal emulator');
+ }
+
if (self::$stty && Terminal::hasSttyAvailable()) {
shell_exec('stty '.$sttyMode);
}
@@ -468,11 +457,9 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $
*
* @param callable $interviewer A callable that will ask for a question and return the result
*
- * @return mixed The validated response
- *
* @throws \Exception In case the max number of attempts has been reached and no valid response has been given
*/
- private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question)
+ private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question): mixed
{
$error = null;
$attempts = $question->getMaxAttempts();
@@ -499,7 +486,7 @@ private function isInteractiveInput($inputStream): bool
return false;
}
- if (null !== self::$stdinIsInteractive) {
+ if (isset(self::$stdinIsInteractive)) {
return self::$stdinIsInteractive;
}
@@ -511,10 +498,8 @@ private function isInteractiveInput($inputStream): bool
*
* @param resource $inputStream The handler resource
* @param Question $question The question being asked
- *
- * @return string|false The input received, false in case input could not be read
*/
- private function readInput($inputStream, Question $question)
+ private function readInput($inputStream, Question $question): string|false
{
if (!$question->isMultiline()) {
$cp = $this->setIOCodepage();
@@ -540,11 +525,6 @@ private function readInput($inputStream, Question $question)
return $this->resetIOCodepage($cp, $ret);
}
- /**
- * Sets console I/O to the host code page.
- *
- * @return int Previous code page in IBM/EBCDIC format
- */
private function setIOCodepage(): int
{
if (\function_exists('sapi_windows_cp_set')) {
@@ -559,12 +539,8 @@ private function setIOCodepage(): int
/**
* Sets console I/O to the specified code page and converts the user input.
- *
- * @param string|false $input
- *
- * @return string|false
*/
- private function resetIOCodepage(int $cp, $input)
+ private function resetIOCodepage(int $cp, string|false $input): string|false
{
if (0 !== $cp) {
sapi_windows_cp_set($cp);
diff --git a/Helper/SymfonyQuestionHelper.php b/Helper/SymfonyQuestionHelper.php
index 01f94aba4..b452bf047 100644
--- a/Helper/SymfonyQuestionHelper.php
+++ b/Helper/SymfonyQuestionHelper.php
@@ -25,26 +25,23 @@
*/
class SymfonyQuestionHelper extends QuestionHelper
{
- /**
- * {@inheritdoc}
- */
- protected function writePrompt(OutputInterface $output, Question $question)
+ protected function writePrompt(OutputInterface $output, Question $question): void
{
$text = OutputFormatter::escapeTrailingBackslash($question->getQuestion());
$default = $question->getDefault();
if ($question->isMultiline()) {
- $text .= sprintf(' (press %s to continue)', $this->getEofShortcut());
+ $text .= \sprintf(' (press %s to continue)', $this->getEofShortcut());
}
switch (true) {
case null === $default:
- $text = sprintf(' %s:', $text);
+ $text = \sprintf(' %s:', $text);
break;
case $question instanceof ConfirmationQuestion:
- $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no');
+ $text = \sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no');
break;
@@ -56,18 +53,18 @@ protected function writePrompt(OutputInterface $output, Question $question)
$default[$key] = $choices[trim($value)];
}
- $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape(implode(', ', $default)));
+ $text = \sprintf(' %s [%s]:', $text, OutputFormatter::escape(implode(', ', $default)));
break;
case $question instanceof ChoiceQuestion:
$choices = $question->getChoices();
- $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape($choices[$default] ?? $default));
+ $text = \sprintf(' %s [%s]:', $text, OutputFormatter::escape($choices[$default] ?? $default));
break;
default:
- $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape($default));
+ $text = \sprintf(' %s [%s]:', $text, OutputFormatter::escape($default));
}
$output->writeln($text);
@@ -83,10 +80,7 @@ protected function writePrompt(OutputInterface $output, Question $question)
$output->write($prompt);
}
- /**
- * {@inheritdoc}
- */
- protected function writeError(OutputInterface $output, \Exception $error)
+ protected function writeError(OutputInterface $output, \Exception $error): void
{
if ($output instanceof SymfonyStyle) {
$output->newLine();
diff --git a/Helper/Table.php b/Helper/Table.php
index 698f9693b..9ff73d2cc 100644
--- a/Helper/Table.php
+++ b/Helper/Table.php
@@ -35,70 +35,29 @@ class Table
private const SEPARATOR_BOTTOM = 3;
private const BORDER_OUTSIDE = 0;
private const BORDER_INSIDE = 1;
-
- private $headerTitle;
- private $footerTitle;
-
- /**
- * Table headers.
- */
- private $headers = [];
-
- /**
- * Table rows.
- */
- private $rows = [];
- private $horizontal = false;
-
- /**
- * Column widths cache.
- */
- private $effectiveColumnWidths = [];
-
- /**
- * Number of columns cache.
- *
- * @var int
- */
- private $numberOfColumns;
-
- /**
- * @var OutputInterface
- */
- private $output;
-
- /**
- * @var TableStyle
- */
- private $style;
-
- /**
- * @var array
- */
- private $columnStyles = [];
-
- /**
- * User set column widths.
- *
- * @var array
- */
- private $columnWidths = [];
- private $columnMaxWidths = [];
-
- /**
- * @var array|null
- */
- private static $styles;
-
- private $rendered = false;
-
- public function __construct(OutputInterface $output)
- {
- $this->output = $output;
-
- if (!self::$styles) {
- self::$styles = self::initStyles();
- }
+ private const DISPLAY_ORIENTATION_DEFAULT = 'default';
+ private const DISPLAY_ORIENTATION_HORIZONTAL = 'horizontal';
+ private const DISPLAY_ORIENTATION_VERTICAL = 'vertical';
+
+ private ?string $headerTitle = null;
+ private ?string $footerTitle = null;
+ private array $headers = [];
+ private array $rows = [];
+ private array $effectiveColumnWidths = [];
+ private int $numberOfColumns;
+ private TableStyle $style;
+ private array $columnStyles = [];
+ private array $columnWidths = [];
+ private array $columnMaxWidths = [];
+ private bool $rendered = false;
+ private string $displayOrientation = self::DISPLAY_ORIENTATION_DEFAULT;
+
+ private static array $styles;
+
+ public function __construct(
+ private OutputInterface $output,
+ ) {
+ self::$styles ??= self::initStyles();
$this->setStyle('default');
}
@@ -106,41 +65,29 @@ public function __construct(OutputInterface $output)
/**
* Sets a style definition.
*/
- public static function setStyleDefinition(string $name, TableStyle $style)
+ public static function setStyleDefinition(string $name, TableStyle $style): void
{
- if (!self::$styles) {
- self::$styles = self::initStyles();
- }
+ self::$styles ??= self::initStyles();
self::$styles[$name] = $style;
}
/**
* Gets a style definition by name.
- *
- * @return TableStyle
*/
- public static function getStyleDefinition(string $name)
+ public static function getStyleDefinition(string $name): TableStyle
{
- if (!self::$styles) {
- self::$styles = self::initStyles();
- }
-
- if (isset(self::$styles[$name])) {
- return self::$styles[$name];
- }
+ self::$styles ??= self::initStyles();
- throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
+ return self::$styles[$name] ?? throw new InvalidArgumentException(\sprintf('Style "%s" is not defined.', $name));
}
/**
* Sets table style.
*
- * @param TableStyle|string $name The style name or a TableStyle instance
- *
* @return $this
*/
- public function setStyle($name)
+ public function setStyle(TableStyle|string $name): static
{
$this->style = $this->resolveStyle($name);
@@ -149,10 +96,8 @@ public function setStyle($name)
/**
* Gets the current table style.
- *
- * @return TableStyle
*/
- public function getStyle()
+ public function getStyle(): TableStyle
{
return $this->style;
}
@@ -164,7 +109,7 @@ public function getStyle()
*
* @return $this
*/
- public function setColumnStyle(int $columnIndex, $name)
+ public function setColumnStyle(int $columnIndex, TableStyle|string $name): static
{
$this->columnStyles[$columnIndex] = $this->resolveStyle($name);
@@ -175,10 +120,8 @@ public function setColumnStyle(int $columnIndex, $name)
* Gets the current style for a column.
*
* If style was not set, it returns the global table style.
- *
- * @return TableStyle
*/
- public function getColumnStyle(int $columnIndex)
+ public function getColumnStyle(int $columnIndex): TableStyle
{
return $this->columnStyles[$columnIndex] ?? $this->getStyle();
}
@@ -188,7 +131,7 @@ public function getColumnStyle(int $columnIndex)
*
* @return $this
*/
- public function setColumnWidth(int $columnIndex, int $width)
+ public function setColumnWidth(int $columnIndex, int $width): static
{
$this->columnWidths[$columnIndex] = $width;
@@ -200,7 +143,7 @@ public function setColumnWidth(int $columnIndex, int $width)
*
* @return $this
*/
- public function setColumnWidths(array $widths)
+ public function setColumnWidths(array $widths): static
{
$this->columnWidths = [];
foreach ($widths as $index => $width) {
@@ -218,10 +161,10 @@ public function setColumnWidths(array $widths)
*
* @return $this
*/
- public function setColumnMaxWidth(int $columnIndex, int $width): self
+ public function setColumnMaxWidth(int $columnIndex, int $width): static
{
if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) {
- throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, get_debug_type($this->output->getFormatter())));
+ throw new \LogicException(\sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, get_debug_type($this->output->getFormatter())));
}
$this->columnMaxWidths[$columnIndex] = $width;
@@ -232,10 +175,10 @@ public function setColumnMaxWidth(int $columnIndex, int $width): self
/**
* @return $this
*/
- public function setHeaders(array $headers)
+ public function setHeaders(array $headers): static
{
$headers = array_values($headers);
- if (!empty($headers) && !\is_array($headers[0])) {
+ if ($headers && !\is_array($headers[0])) {
$headers = [$headers];
}
@@ -244,7 +187,10 @@ public function setHeaders(array $headers)
return $this;
}
- public function setRows(array $rows)
+ /**
+ * @return $this
+ */
+ public function setRows(array $rows): static
{
$this->rows = [];
@@ -254,7 +200,7 @@ public function setRows(array $rows)
/**
* @return $this
*/
- public function addRows(array $rows)
+ public function addRows(array $rows): static
{
foreach ($rows as $row) {
$this->addRow($row);
@@ -266,7 +212,7 @@ public function addRows(array $rows)
/**
* @return $this
*/
- public function addRow($row)
+ public function addRow(TableSeparator|array $row): static
{
if ($row instanceof TableSeparator) {
$this->rows[] = $row;
@@ -274,10 +220,6 @@ public function addRow($row)
return $this;
}
- if (!\is_array($row)) {
- throw new InvalidArgumentException('A row must be an array or a TableSeparator instance.');
- }
-
$this->rows[] = array_values($row);
return $this;
@@ -288,10 +230,10 @@ public function addRow($row)
*
* @return $this
*/
- public function appendRow($row): self
+ public function appendRow(TableSeparator|array $row): static
{
if (!$this->output instanceof ConsoleSectionOutput) {
- throw new RuntimeException(sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__));
+ throw new RuntimeException(\sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__));
}
if ($this->rendered) {
@@ -307,7 +249,7 @@ public function appendRow($row): self
/**
* @return $this
*/
- public function setRow($column, array $row)
+ public function setRow(int|string $column, array $row): static
{
$this->rows[$column] = $row;
@@ -317,7 +259,7 @@ public function setRow($column, array $row)
/**
* @return $this
*/
- public function setHeaderTitle(?string $title): self
+ public function setHeaderTitle(?string $title): static
{
$this->headerTitle = $title;
@@ -327,7 +269,7 @@ public function setHeaderTitle(?string $title): self
/**
* @return $this
*/
- public function setFooterTitle(?string $title): self
+ public function setFooterTitle(?string $title): static
{
$this->footerTitle = $title;
@@ -337,9 +279,19 @@ public function setFooterTitle(?string $title): self
/**
* @return $this
*/
- public function setHorizontal(bool $horizontal = true): self
+ public function setHorizontal(bool $horizontal = true): static
{
- $this->horizontal = $horizontal;
+ $this->displayOrientation = $horizontal ? self::DISPLAY_ORIENTATION_HORIZONTAL : self::DISPLAY_ORIENTATION_DEFAULT;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setVertical(bool $vertical = true): static
+ {
+ $this->displayOrientation = $vertical ? self::DISPLAY_ORIENTATION_VERTICAL : self::DISPLAY_ORIENTATION_DEFAULT;
return $this;
}
@@ -357,11 +309,16 @@ public function setHorizontal(bool $horizontal = true): self
* | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
* +---------------+-----------------------+------------------+
*/
- public function render()
+ public function render(): void
{
$divider = new TableSeparator();
- if ($this->horizontal) {
- $rows = [];
+ $isCellWithColspan = static fn ($cell) => $cell instanceof TableCell && $cell->getColspan() >= 2;
+
+ $horizontal = self::DISPLAY_ORIENTATION_HORIZONTAL === $this->displayOrientation;
+ $vertical = self::DISPLAY_ORIENTATION_VERTICAL === $this->displayOrientation;
+
+ $rows = [];
+ if ($horizontal) {
foreach ($this->headers[0] ?? [] as $i => $header) {
$rows[$i] = [$header];
foreach ($this->rows as $row) {
@@ -370,13 +327,62 @@ public function render()
}
if (isset($row[$i])) {
$rows[$i][] = $row[$i];
- } elseif ($rows[$i][0] instanceof TableCell && $rows[$i][0]->getColspan() >= 2) {
+ } elseif ($isCellWithColspan($rows[$i][0])) {
// Noop, there is a "title"
} else {
$rows[$i][] = null;
}
}
}
+ } elseif ($vertical) {
+ $formatter = $this->output->getFormatter();
+ $maxHeaderLength = array_reduce($this->headers[0] ?? [], static fn ($max, $header) => max($max, Helper::width(Helper::removeDecoration($formatter, $header))), 0);
+
+ foreach ($this->rows as $row) {
+ if ($row instanceof TableSeparator) {
+ continue;
+ }
+
+ if ($rows) {
+ $rows[] = [$divider];
+ }
+
+ $containsColspan = false;
+ foreach ($row as $cell) {
+ if ($containsColspan = $isCellWithColspan($cell)) {
+ break;
+ }
+ }
+
+ $headers = $this->headers[0] ?? [];
+ $maxRows = max(\count($headers), \count($row));
+ for ($i = 0; $i < $maxRows; ++$i) {
+ $cell = (string) ($row[$i] ?? '');
+
+ $eol = str_contains($cell, "\r\n") ? "\r\n" : "\n";
+ $parts = explode($eol, $cell);
+ foreach ($parts as $idx => $part) {
+ if ($headers && !$containsColspan) {
+ if (0 === $idx) {
+ $rows[] = [\sprintf(
+ '%s%s>: %s',
+ str_repeat(' ', $maxHeaderLength - Helper::width(Helper::removeDecoration($formatter, $headers[$i] ?? ''))),
+ $headers[$i] ?? '',
+ $part
+ )];
+ } else {
+ $rows[] = [\sprintf(
+ '%s %s',
+ str_pad('', $maxHeaderLength, ' ', \STR_PAD_LEFT),
+ $part
+ )];
+ }
+ } elseif ('' !== $cell) {
+ $rows[] = [$part];
+ }
+ }
+ }
+ }
} else {
$rows = array_merge($this->headers, [$divider], $this->rows);
}
@@ -386,8 +392,8 @@ public function render()
$rowGroups = $this->buildTableRows($rows);
$this->calculateColumnsWidth($rowGroups);
- $isHeader = !$this->horizontal;
- $isFirstRow = $this->horizontal;
+ $isHeader = !$horizontal;
+ $isFirstRow = $horizontal;
$hasTitle = (bool) $this->headerTitle;
foreach ($rowGroups as $rowGroup) {
@@ -413,7 +419,7 @@ public function render()
if ($isHeader && !$isHeaderSeparatorRendered) {
$this->renderRowSeparator(
- $isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
+ self::SEPARATOR_TOP,
$hasTitle ? $this->headerTitle : null,
$hasTitle ? $this->style->getHeaderTitleFormat() : null
);
@@ -423,7 +429,7 @@ public function render()
if ($isFirstRow) {
$this->renderRowSeparator(
- $isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
+ $horizontal ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
$hasTitle ? $this->headerTitle : null,
$hasTitle ? $this->style->getHeaderTitleFormat() : null
);
@@ -431,7 +437,12 @@ public function render()
$hasTitle = false;
}
- if ($this->horizontal) {
+ if ($vertical) {
+ $isHeader = false;
+ $isFirstRow = false;
+ }
+
+ if ($horizontal) {
$this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat());
} else {
$this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat());
@@ -451,9 +462,9 @@ public function render()
*
* +-----+-----------+-------+
*/
- private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null)
+ private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null): void
{
- if (0 === $count = $this->numberOfColumns) {
+ if (!$count = $this->numberOfColumns) {
return;
}
@@ -480,12 +491,12 @@ private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $ti
}
if (null !== $title) {
- $titleLength = Helper::width(Helper::removeDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title)));
+ $titleLength = Helper::width(Helper::removeDecoration($formatter = $this->output->getFormatter(), $formattedTitle = \sprintf($titleFormat, $title)));
$markupLength = Helper::width($markup);
if ($titleLength > $limit = $markupLength - 4) {
$titleLength = $limit;
- $formatLength = Helper::width(Helper::removeDecoration($formatter, sprintf($titleFormat, '')));
- $formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...');
+ $formatLength = Helper::width(Helper::removeDecoration($formatter, \sprintf($titleFormat, '')));
+ $formattedTitle = \sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...');
}
$titleStart = intdiv($markupLength - $titleLength, 2);
@@ -496,7 +507,7 @@ private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $ti
}
}
- $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup));
+ $this->output->writeln(\sprintf($this->style->getBorderFormat(), $markup));
}
/**
@@ -506,7 +517,7 @@ private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string
{
$borders = $this->style->getBorderChars();
- return sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]);
+ return \sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]);
}
/**
@@ -516,7 +527,7 @@ private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string
*
* | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
*/
- private function renderRow(array $row, string $cellFormat, ?string $firstCellFormat = null)
+ private function renderRow(array $row, string $cellFormat, ?string $firstCellFormat = null): void
{
$rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE);
$columns = $this->getRowColumns($row);
@@ -554,11 +565,11 @@ private function renderCell(array $row, int $column, string $cellFormat): string
$style = $this->getColumnStyle($column);
if ($cell instanceof TableSeparator) {
- return sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width));
+ return \sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width));
}
$width += Helper::length($cell) - Helper::length(Helper::removeDecoration($this->output->getFormatter(), $cell));
- $content = sprintf($style->getCellRowContentFormat(), $cell);
+ $content = \sprintf($style->getCellRowContentFormat(), $cell);
$padType = $style->getPadType();
if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) {
@@ -570,11 +581,11 @@ private function renderCell(array $row, int $column, string $cellFormat): string
$cellFormat = '<'.$tag.'>%s>';
}
- if (strstr($content, '>')) {
+ if (str_contains($content, '>')) {
$content = str_replace('>', '', $content);
$width -= 3;
}
- if (strstr($content, '')) {
+ if (str_contains($content, '')) {
$content = str_replace('', '', $content);
$width -= \strlen('');
}
@@ -583,13 +594,13 @@ private function renderCell(array $row, int $column, string $cellFormat): string
$padType = $cell->getStyle()->getPadByAlign();
}
- return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType));
+ return \sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType));
}
/**
* Calculate number of columns for this table.
*/
- private function calculateNumberOfColumns(array $rows)
+ private function calculateNumberOfColumns(array $rows): void
{
$columns = [0];
foreach ($rows as $row) {
@@ -618,11 +629,11 @@ private function buildTableRows(array $rows): TableRows
if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) {
$cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
}
- if (!strstr($cell ?? '', "\n")) {
+ if (!str_contains($cell ?? '', "\n")) {
continue;
}
$eol = str_contains($cell ?? '', "\r\n") ? "\r\n" : "\n";
- $escaped = implode($eol, array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode($eol, $cell)));
+ $escaped = implode($eol, array_map(OutputFormatter::escapeTrailingBackslash(...), explode($eol, $cell)));
$cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped;
$lines = explode($eol, str_replace($eol, '>'.$eol, $cell));
foreach ($lines as $lineKey => $line) {
@@ -663,7 +674,7 @@ private function calculateRowCount(): int
++$numberOfRows; // Add row for header separator
}
- if (\count($this->rows) > 0) {
+ if ($this->rows) {
++$numberOfRows; // Add row for footer separator
}
@@ -679,13 +690,13 @@ private function fillNextRows(array $rows, int $line): array
{
$unmergedRows = [];
foreach ($rows[$line] as $column => $cell) {
- if (null !== $cell && !$cell instanceof TableCell && !\is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) {
- throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell)));
+ if (null !== $cell && !$cell instanceof TableCell && !\is_scalar($cell) && !$cell instanceof \Stringable) {
+ throw new InvalidArgumentException(\sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell)));
}
if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
$nbLines = $cell->getRowspan() - 1;
$lines = [$cell];
- if (strstr($cell, "\n")) {
+ if (str_contains($cell, "\n")) {
$eol = str_contains($cell, "\r\n") ? "\r\n" : "\n";
$lines = explode($eol, str_replace($eol, ''.$eol.'>', $cell));
$nbLines = \count($lines) > $nbLines ? substr_count($cell, $eol) : $nbLines;
@@ -708,7 +719,7 @@ private function fillNextRows(array $rows, int $line): array
foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
// we need to know if $unmergedRow will be merged or inserted into $rows
- if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) {
+ if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRow) <= $this->numberOfColumns)) {
foreach ($unmergedRow as $cellKey => $cell) {
// insert cell into row at cellKey position
array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]);
@@ -716,8 +727,8 @@ private function fillNextRows(array $rows, int $line): array
} else {
$row = $this->copyRow($rows, $unmergedRowKey - 1);
foreach ($unmergedRow as $column => $cell) {
- if (!empty($cell)) {
- $row[$column] = $unmergedRow[$column];
+ if ($cell) {
+ $row[$column] = $cell;
}
}
array_splice($rows, $unmergedRowKey, 0, [$row]);
@@ -730,7 +741,7 @@ private function fillNextRows(array $rows, int $line): array
/**
* fill cells for a row that contains colspan > 1.
*/
- private function fillCells(iterable $row)
+ private function fillCells(iterable $row): iterable
{
$newRow = [];
@@ -792,7 +803,7 @@ private function getRowColumns(array $row): array
/**
* Calculates columns widths.
*/
- private function calculateColumnsWidth(iterable $groups)
+ private function calculateColumnsWidth(iterable $groups): void
{
for ($column = 0; $column < $this->numberOfColumns; ++$column) {
$lengths = [];
@@ -825,7 +836,7 @@ private function calculateColumnsWidth(iterable $groups)
private function getColumnSeparatorWidth(): int
{
- return Helper::width(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
+ return Helper::width(\sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
}
private function getCellWidth(array $row, int $column): int
@@ -846,10 +857,10 @@ private function getCellWidth(array $row, int $column): int
/**
* Called after rendering to cleanup cache data.
*/
- private function cleanup()
+ private function cleanup(): void
{
$this->effectiveColumnWidths = [];
- $this->numberOfColumns = null;
+ unset($this->numberOfColumns);
}
/**
@@ -902,16 +913,12 @@ private static function initStyles(): array
];
}
- private function resolveStyle($name): TableStyle
+ private function resolveStyle(TableStyle|string $name): TableStyle
{
if ($name instanceof TableStyle) {
return $name;
}
- if (isset(self::$styles[$name])) {
- return self::$styles[$name];
- }
-
- throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
+ return self::$styles[$name] ?? throw new InvalidArgumentException(\sprintf('Style "%s" is not defined.', $name));
}
}
diff --git a/Helper/TableCell.php b/Helper/TableCell.php
index 1a7bc6ede..ab8339204 100644
--- a/Helper/TableCell.php
+++ b/Helper/TableCell.php
@@ -18,20 +18,19 @@
*/
class TableCell
{
- private $value;
- private $options = [
+ private array $options = [
'rowspan' => 1,
'colspan' => 1,
'style' => null,
];
- public function __construct(string $value = '', array $options = [])
- {
- $this->value = $value;
-
+ public function __construct(
+ private string $value = '',
+ array $options = [],
+ ) {
// check option names
if ($diff = array_diff(array_keys($options), array_keys($this->options))) {
- throw new InvalidArgumentException(sprintf('The TableCell does not support the following options: \'%s\'.', implode('\', \'', $diff)));
+ throw new InvalidArgumentException(\sprintf('The TableCell does not support the following options: \'%s\'.', implode('\', \'', $diff)));
}
if (isset($options['style']) && !$options['style'] instanceof TableCellStyle) {
@@ -43,30 +42,24 @@ public function __construct(string $value = '', array $options = [])
/**
* Returns the cell value.
- *
- * @return string
*/
- public function __toString()
+ public function __toString(): string
{
return $this->value;
}
/**
* Gets number of colspan.
- *
- * @return int
*/
- public function getColspan()
+ public function getColspan(): int
{
return (int) $this->options['colspan'];
}
/**
* Gets number of rowspan.
- *
- * @return int
*/
- public function getRowspan()
+ public function getRowspan(): int
{
return (int) $this->options['rowspan'];
}
diff --git a/Helper/TableCellStyle.php b/Helper/TableCellStyle.php
index 19cd0ffc6..af1a17e96 100644
--- a/Helper/TableCellStyle.php
+++ b/Helper/TableCellStyle.php
@@ -32,7 +32,7 @@ class TableCellStyle
'right' => \STR_PAD_LEFT,
];
- private $options = [
+ private array $options = [
'fg' => 'default',
'bg' => 'default',
'options' => null,
@@ -43,11 +43,11 @@ class TableCellStyle
public function __construct(array $options = [])
{
if ($diff = array_diff(array_keys($options), array_keys($this->options))) {
- throw new InvalidArgumentException(sprintf('The TableCellStyle does not support the following options: \'%s\'.', implode('\', \'', $diff)));
+ throw new InvalidArgumentException(\sprintf('The TableCellStyle does not support the following options: \'%s\'.', implode('\', \'', $diff)));
}
if (isset($options['align']) && !\array_key_exists($options['align'], self::ALIGN_MAP)) {
- throw new InvalidArgumentException(sprintf('Wrong align value. Value must be following: \'%s\'.', implode('\', \'', array_keys(self::ALIGN_MAP))));
+ throw new InvalidArgumentException(\sprintf('Wrong align value. Value must be following: \'%s\'.', implode('\', \'', array_keys(self::ALIGN_MAP))));
}
$this->options = array_merge($this->options, $options);
@@ -63,21 +63,16 @@ public function getOptions(): array
*
* @return string[]
*/
- public function getTagOptions()
+ public function getTagOptions(): array
{
return array_filter(
$this->getOptions(),
- function ($key) {
- return \in_array($key, self::TAG_OPTIONS) && isset($this->options[$key]);
- },
+ fn ($key) => \in_array($key, self::TAG_OPTIONS, true) && isset($this->options[$key]),
\ARRAY_FILTER_USE_KEY
);
}
- /**
- * @return int
- */
- public function getPadByAlign()
+ public function getPadByAlign(): int
{
return self::ALIGN_MAP[$this->getOptions()['align']];
}
diff --git a/Helper/TableRows.php b/Helper/TableRows.php
index cbc07d294..fb2dc2789 100644
--- a/Helper/TableRows.php
+++ b/Helper/TableRows.php
@@ -16,11 +16,9 @@
*/
class TableRows implements \IteratorAggregate
{
- private $generator;
-
- public function __construct(\Closure $generator)
- {
- $this->generator = $generator;
+ public function __construct(
+ private \Closure $generator,
+ ) {
}
public function getIterator(): \Traversable
diff --git a/Helper/TableStyle.php b/Helper/TableStyle.php
index 0643c79eb..be956c109 100644
--- a/Helper/TableStyle.php
+++ b/Helper/TableStyle.php
@@ -23,37 +23,37 @@
*/
class TableStyle
{
- private $paddingChar = ' ';
- private $horizontalOutsideBorderChar = '-';
- private $horizontalInsideBorderChar = '-';
- private $verticalOutsideBorderChar = '|';
- private $verticalInsideBorderChar = '|';
- private $crossingChar = '+';
- private $crossingTopRightChar = '+';
- private $crossingTopMidChar = '+';
- private $crossingTopLeftChar = '+';
- private $crossingMidRightChar = '+';
- private $crossingBottomRightChar = '+';
- private $crossingBottomMidChar = '+';
- private $crossingBottomLeftChar = '+';
- private $crossingMidLeftChar = '+';
- private $crossingTopLeftBottomChar = '+';
- private $crossingTopMidBottomChar = '+';
- private $crossingTopRightBottomChar = '+';
- private $headerTitleFormat = ' %s >';
- private $footerTitleFormat = ' %s >';
- private $cellHeaderFormat = '%s';
- private $cellRowFormat = '%s';
- private $cellRowContentFormat = ' %s ';
- private $borderFormat = '%s';
- private $padType = \STR_PAD_RIGHT;
+ private string $paddingChar = ' ';
+ private string $horizontalOutsideBorderChar = '-';
+ private string $horizontalInsideBorderChar = '-';
+ private string $verticalOutsideBorderChar = '|';
+ private string $verticalInsideBorderChar = '|';
+ private string $crossingChar = '+';
+ private string $crossingTopRightChar = '+';
+ private string $crossingTopMidChar = '+';
+ private string $crossingTopLeftChar = '+';
+ private string $crossingMidRightChar = '+';
+ private string $crossingBottomRightChar = '+';
+ private string $crossingBottomMidChar = '+';
+ private string $crossingBottomLeftChar = '+';
+ private string $crossingMidLeftChar = '+';
+ private string $crossingTopLeftBottomChar = '+';
+ private string $crossingTopMidBottomChar = '+';
+ private string $crossingTopRightBottomChar = '+';
+ private string $headerTitleFormat = ' %s >';
+ private string $footerTitleFormat = ' %s >';
+ private string $cellHeaderFormat = '%s';
+ private string $cellRowFormat = '%s';
+ private string $cellRowContentFormat = ' %s ';
+ private string $borderFormat = '%s';
+ private int $padType = \STR_PAD_RIGHT;
/**
* Sets padding character, used for cell padding.
*
* @return $this
*/
- public function setPaddingChar(string $paddingChar)
+ public function setPaddingChar(string $paddingChar): static
{
if (!$paddingChar) {
throw new LogicException('The padding char must not be empty.');
@@ -66,10 +66,8 @@ public function setPaddingChar(string $paddingChar)
/**
* Gets padding character, used for cell padding.
- *
- * @return string
*/
- public function getPaddingChar()
+ public function getPaddingChar(): string
{
return $this->paddingChar;
}
@@ -90,7 +88,7 @@ public function getPaddingChar()
*
* @return $this
*/
- public function setHorizontalBorderChars(string $outside, ?string $inside = null): self
+ public function setHorizontalBorderChars(string $outside, ?string $inside = null): static
{
$this->horizontalOutsideBorderChar = $outside;
$this->horizontalInsideBorderChar = $inside ?? $outside;
@@ -115,7 +113,7 @@ public function setHorizontalBorderChars(string $outside, ?string $inside = null
*
* @return $this
*/
- public function setVerticalBorderChars(string $outside, ?string $inside = null): self
+ public function setVerticalBorderChars(string $outside, ?string $inside = null): static
{
$this->verticalOutsideBorderChar = $outside;
$this->verticalInsideBorderChar = $inside ?? $outside;
@@ -169,7 +167,7 @@ public function getBorderChars(): array
*
* @return $this
*/
- public function setCrossingChars(string $cross, string $topLeft, string $topMid, string $topRight, string $midRight, string $bottomRight, string $bottomMid, string $bottomLeft, string $midLeft, ?string $topLeftBottom = null, ?string $topMidBottom = null, ?string $topRightBottom = null): self
+ public function setCrossingChars(string $cross, string $topLeft, string $topMid, string $topRight, string $midRight, string $bottomRight, string $bottomMid, string $bottomLeft, string $midLeft, ?string $topLeftBottom = null, ?string $topMidBottom = null, ?string $topRightBottom = null): static
{
$this->crossingChar = $cross;
$this->crossingTopLeftChar = $topLeft;
@@ -199,10 +197,8 @@ public function setDefaultCrossingChar(string $char): self
/**
* Gets crossing character.
- *
- * @return string
*/
- public function getCrossingChar()
+ public function getCrossingChar(): string
{
return $this->crossingChar;
}
@@ -235,7 +231,7 @@ public function getCrossingChars(): array
*
* @return $this
*/
- public function setCellHeaderFormat(string $cellHeaderFormat)
+ public function setCellHeaderFormat(string $cellHeaderFormat): static
{
$this->cellHeaderFormat = $cellHeaderFormat;
@@ -244,10 +240,8 @@ public function setCellHeaderFormat(string $cellHeaderFormat)
/**
* Gets header cell format.
- *
- * @return string
*/
- public function getCellHeaderFormat()
+ public function getCellHeaderFormat(): string
{
return $this->cellHeaderFormat;
}
@@ -257,7 +251,7 @@ public function getCellHeaderFormat()
*
* @return $this
*/
- public function setCellRowFormat(string $cellRowFormat)
+ public function setCellRowFormat(string $cellRowFormat): static
{
$this->cellRowFormat = $cellRowFormat;
@@ -266,10 +260,8 @@ public function setCellRowFormat(string $cellRowFormat)
/**
* Gets row cell format.
- *
- * @return string
*/
- public function getCellRowFormat()
+ public function getCellRowFormat(): string
{
return $this->cellRowFormat;
}
@@ -279,7 +271,7 @@ public function getCellRowFormat()
*
* @return $this
*/
- public function setCellRowContentFormat(string $cellRowContentFormat)
+ public function setCellRowContentFormat(string $cellRowContentFormat): static
{
$this->cellRowContentFormat = $cellRowContentFormat;
@@ -288,10 +280,8 @@ public function setCellRowContentFormat(string $cellRowContentFormat)
/**
* Gets row cell content format.
- *
- * @return string
*/
- public function getCellRowContentFormat()
+ public function getCellRowContentFormat(): string
{
return $this->cellRowContentFormat;
}
@@ -301,7 +291,7 @@ public function getCellRowContentFormat()
*
* @return $this
*/
- public function setBorderFormat(string $borderFormat)
+ public function setBorderFormat(string $borderFormat): static
{
$this->borderFormat = $borderFormat;
@@ -310,10 +300,8 @@ public function setBorderFormat(string $borderFormat)
/**
* Gets table border format.
- *
- * @return string
*/
- public function getBorderFormat()
+ public function getBorderFormat(): string
{
return $this->borderFormat;
}
@@ -323,7 +311,7 @@ public function getBorderFormat()
*
* @return $this
*/
- public function setPadType(int $padType)
+ public function setPadType(int $padType): static
{
if (!\in_array($padType, [\STR_PAD_LEFT, \STR_PAD_RIGHT, \STR_PAD_BOTH], true)) {
throw new InvalidArgumentException('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).');
@@ -336,10 +324,8 @@ public function setPadType(int $padType)
/**
* Gets cell padding type.
- *
- * @return int
*/
- public function getPadType()
+ public function getPadType(): int
{
return $this->padType;
}
@@ -352,7 +338,7 @@ public function getHeaderTitleFormat(): string
/**
* @return $this
*/
- public function setHeaderTitleFormat(string $format): self
+ public function setHeaderTitleFormat(string $format): static
{
$this->headerTitleFormat = $format;
@@ -367,7 +353,7 @@ public function getFooterTitleFormat(): string
/**
* @return $this
*/
- public function setFooterTitleFormat(string $format): self
+ public function setFooterTitleFormat(string $format): static
{
$this->footerTitleFormat = $format;
diff --git a/Input/ArgvInput.php b/Input/ArgvInput.php
index 0c4b2d25b..fe25b861a 100644
--- a/Input/ArgvInput.php
+++ b/Input/ArgvInput.php
@@ -40,12 +40,20 @@
*/
class ArgvInput extends Input
{
- private $tokens;
- private $parsed;
+ /** @var list */
+ private array $tokens;
+ private array $parsed;
+ /** @param list|null $argv */
public function __construct(?array $argv = null, ?InputDefinition $definition = null)
{
- $argv = $argv ?? $_SERVER['argv'] ?? [];
+ $argv ??= $_SERVER['argv'] ?? [];
+
+ foreach ($argv as $arg) {
+ if (!\is_scalar($arg) && !$arg instanceof \Stringable) {
+ throw new RuntimeException(\sprintf('Argument values expected to be all scalars, got "%s".', get_debug_type($arg)));
+ }
+ }
// strip the application name
array_shift($argv);
@@ -55,15 +63,13 @@ public function __construct(?array $argv = null, ?InputDefinition $definition =
parent::__construct($definition);
}
- protected function setTokens(array $tokens)
+ /** @param list $tokens */
+ protected function setTokens(array $tokens): void
{
$this->tokens = $tokens;
}
- /**
- * {@inheritdoc}
- */
- protected function parse()
+ protected function parse(): void
{
$parseOptions = true;
$this->parsed = $this->tokens;
@@ -92,7 +98,7 @@ protected function parseToken(string $token, bool $parseOptions): bool
/**
* Parses a short option.
*/
- private function parseShortOption(string $token)
+ private function parseShortOption(string $token): void
{
$name = substr($token, 1);
@@ -113,13 +119,13 @@ private function parseShortOption(string $token)
*
* @throws RuntimeException When option given doesn't exist
*/
- private function parseShortOptionSet(string $name)
+ private function parseShortOptionSet(string $name): void
{
$len = \strlen($name);
for ($i = 0; $i < $len; ++$i) {
if (!$this->definition->hasShortcut($name[$i])) {
$encoding = mb_detect_encoding($name, null, true);
- throw new RuntimeException(sprintf('The "-%s" option does not exist.', false === $encoding ? $name[$i] : mb_substr($name, $i, 1, $encoding)));
+ throw new RuntimeException(\sprintf('The "-%s" option does not exist.', false === $encoding ? $name[$i] : mb_substr($name, $i, 1, $encoding)));
}
$option = $this->definition->getOptionForShortcut($name[$i]);
@@ -127,16 +133,16 @@ private function parseShortOptionSet(string $name)
$this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1));
break;
- } else {
- $this->addLongOption($option->getName(), null);
}
+
+ $this->addLongOption($option->getName(), null);
}
}
/**
* Parses a long option.
*/
- private function parseLongOption(string $token)
+ private function parseLongOption(string $token): void
{
$name = substr($token, 2);
@@ -155,7 +161,7 @@ private function parseLongOption(string $token)
*
* @throws RuntimeException When too many arguments are given
*/
- private function parseArgument(string $token)
+ private function parseArgument(string $token): void
{
$c = \count($this->arguments);
@@ -180,14 +186,14 @@ private function parseArgument(string $token)
if (\count($all)) {
if ($symfonyCommandName) {
- $message = sprintf('Too many arguments to "%s" command, expected arguments "%s".', $symfonyCommandName, implode('" "', array_keys($all)));
+ $message = \sprintf('Too many arguments to "%s" command, expected arguments "%s".', $symfonyCommandName, implode('" "', array_keys($all)));
} else {
- $message = sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all)));
+ $message = \sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all)));
}
} elseif ($symfonyCommandName) {
- $message = sprintf('No arguments expected for "%s" command, got "%s".', $symfonyCommandName, $token);
+ $message = \sprintf('No arguments expected for "%s" command, got "%s".', $symfonyCommandName, $token);
} else {
- $message = sprintf('No arguments expected, got "%s".', $token);
+ $message = \sprintf('No arguments expected, got "%s".', $token);
}
throw new RuntimeException($message);
@@ -199,10 +205,10 @@ private function parseArgument(string $token)
*
* @throws RuntimeException When option given doesn't exist
*/
- private function addShortOption(string $shortcut, $value)
+ private function addShortOption(string $shortcut, mixed $value): void
{
if (!$this->definition->hasShortcut($shortcut)) {
- throw new RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut));
+ throw new RuntimeException(\sprintf('The "-%s" option does not exist.', $shortcut));
}
$this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
@@ -213,16 +219,16 @@ private function addShortOption(string $shortcut, $value)
*
* @throws RuntimeException When option given doesn't exist
*/
- private function addLongOption(string $name, $value)
+ private function addLongOption(string $name, mixed $value): void
{
if (!$this->definition->hasOption($name)) {
if (!$this->definition->hasNegation($name)) {
- throw new RuntimeException(sprintf('The "--%s" option does not exist.', $name));
+ throw new RuntimeException(\sprintf('The "--%s" option does not exist.', $name));
}
$optionName = $this->definition->negationToName($name);
if (null !== $value) {
- throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name));
+ throw new RuntimeException(\sprintf('The "--%s" option does not accept a value.', $name));
}
$this->options[$optionName] = false;
@@ -232,7 +238,7 @@ private function addLongOption(string $name, $value)
$option = $this->definition->getOption($name);
if (null !== $value && !$option->acceptValue()) {
- throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name));
+ throw new RuntimeException(\sprintf('The "--%s" option does not accept a value.', $name));
}
if (\in_array($value, ['', null], true) && $option->acceptValue() && \count($this->parsed)) {
@@ -248,7 +254,7 @@ private function addLongOption(string $name, $value)
if (null === $value) {
if ($option->isValueRequired()) {
- throw new RuntimeException(sprintf('The "--%s" option requires a value.', $name));
+ throw new RuntimeException(\sprintf('The "--%s" option requires a value.', $name));
}
if (!$option->isArray() && !$option->isValueOptional()) {
@@ -263,10 +269,7 @@ private function addLongOption(string $name, $value)
}
}
- /**
- * {@inheritdoc}
- */
- public function getFirstArgument()
+ public function getFirstArgument(): ?string
{
$isOption = false;
foreach ($this->tokens as $i => $token) {
@@ -298,10 +301,7 @@ public function getFirstArgument()
return null;
}
- /**
- * {@inheritdoc}
- */
- public function hasParameterOption($values, bool $onlyParams = false)
+ public function hasParameterOption(string|array $values, bool $onlyParams = false): bool
{
$values = (array) $values;
@@ -323,10 +323,7 @@ public function hasParameterOption($values, bool $onlyParams = false)
return false;
}
- /**
- * {@inheritdoc}
- */
- public function getParameterOption($values, $default = false, bool $onlyParams = false)
+ public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed
{
$values = (array) $values;
$tokens = $this->tokens;
@@ -355,11 +352,38 @@ public function getParameterOption($values, $default = false, bool $onlyParams =
}
/**
- * Returns a stringified representation of the args passed to the command.
+ * Returns un-parsed and not validated tokens.
+ *
+ * @param bool $strip Whether to return the raw parameters (false) or the values after the command name (true)
*
- * @return string
+ * @return list
+ */
+ public function getRawTokens(bool $strip = false): array
+ {
+ if (!$strip) {
+ return $this->tokens;
+ }
+
+ $parameters = [];
+ $keep = false;
+ foreach ($this->tokens as $value) {
+ if (!$keep && $value === $this->getFirstArgument()) {
+ $keep = true;
+
+ continue;
+ }
+ if ($keep) {
+ $parameters[] = $value;
+ }
+ }
+
+ return $parameters;
+ }
+
+ /**
+ * Returns a stringified representation of the args passed to the command.
*/
- public function __toString()
+ public function __toString(): string
{
$tokens = array_map(function ($token) {
if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) {
diff --git a/Input/ArrayInput.php b/Input/ArrayInput.php
index 21a517cfb..7335632bf 100644
--- a/Input/ArrayInput.php
+++ b/Input/ArrayInput.php
@@ -25,19 +25,14 @@
*/
class ArrayInput extends Input
{
- private $parameters;
-
- public function __construct(array $parameters, ?InputDefinition $definition = null)
- {
- $this->parameters = $parameters;
-
+ public function __construct(
+ private array $parameters,
+ ?InputDefinition $definition = null,
+ ) {
parent::__construct($definition);
}
- /**
- * {@inheritdoc}
- */
- public function getFirstArgument()
+ public function getFirstArgument(): ?string
{
foreach ($this->parameters as $param => $value) {
if ($param && \is_string($param) && '-' === $param[0]) {
@@ -50,10 +45,7 @@ public function getFirstArgument()
return null;
}
- /**
- * {@inheritdoc}
- */
- public function hasParameterOption($values, bool $onlyParams = false)
+ public function hasParameterOption(string|array $values, bool $onlyParams = false): bool
{
$values = (array) $values;
@@ -74,10 +66,7 @@ public function hasParameterOption($values, bool $onlyParams = false)
return false;
}
- /**
- * {@inheritdoc}
- */
- public function getParameterOption($values, $default = false, bool $onlyParams = false)
+ public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed
{
$values = (array) $values;
@@ -100,10 +89,8 @@ public function getParameterOption($values, $default = false, bool $onlyParams =
/**
* Returns a stringified representation of the args passed to the command.
- *
- * @return string
*/
- public function __toString()
+ public function __toString(): string
{
$params = [];
foreach ($this->parameters as $param => $val) {
@@ -117,17 +104,14 @@ public function __toString()
$params[] = $param.('' != $val ? $glue.$this->escapeToken($val) : '');
}
} else {
- $params[] = \is_array($val) ? implode(' ', array_map([$this, 'escapeToken'], $val)) : $this->escapeToken($val);
+ $params[] = \is_array($val) ? implode(' ', array_map($this->escapeToken(...), $val)) : $this->escapeToken($val);
}
}
return implode(' ', $params);
}
- /**
- * {@inheritdoc}
- */
- protected function parse()
+ protected function parse(): void
{
foreach ($this->parameters as $key => $value) {
if ('--' === $key) {
@@ -148,10 +132,10 @@ protected function parse()
*
* @throws InvalidOptionException When option given doesn't exist
*/
- private function addShortOption(string $shortcut, $value)
+ private function addShortOption(string $shortcut, mixed $value): void
{
if (!$this->definition->hasShortcut($shortcut)) {
- throw new InvalidOptionException(sprintf('The "-%s" option does not exist.', $shortcut));
+ throw new InvalidOptionException(\sprintf('The "-%s" option does not exist.', $shortcut));
}
$this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value);
@@ -163,11 +147,11 @@ private function addShortOption(string $shortcut, $value)
* @throws InvalidOptionException When option given doesn't exist
* @throws InvalidOptionException When a required value is missing
*/
- private function addLongOption(string $name, $value)
+ private function addLongOption(string $name, mixed $value): void
{
if (!$this->definition->hasOption($name)) {
if (!$this->definition->hasNegation($name)) {
- throw new InvalidOptionException(sprintf('The "--%s" option does not exist.', $name));
+ throw new InvalidOptionException(\sprintf('The "--%s" option does not exist.', $name));
}
$optionName = $this->definition->negationToName($name);
@@ -180,7 +164,7 @@ private function addLongOption(string $name, $value)
if (null === $value) {
if ($option->isValueRequired()) {
- throw new InvalidOptionException(sprintf('The "--%s" option requires a value.', $name));
+ throw new InvalidOptionException(\sprintf('The "--%s" option requires a value.', $name));
}
if (!$option->isValueOptional()) {
@@ -194,15 +178,12 @@ private function addLongOption(string $name, $value)
/**
* Adds an argument value.
*
- * @param string|int $name The argument name
- * @param mixed $value The value for the argument
- *
* @throws InvalidArgumentException When argument given doesn't exist
*/
- private function addArgument($name, $value)
+ private function addArgument(string|int $name, mixed $value): void
{
if (!$this->definition->hasArgument($name)) {
- throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+ throw new InvalidArgumentException(\sprintf('The "%s" argument does not exist.', $name));
}
$this->arguments[$name] = $value;
diff --git a/Input/Input.php b/Input/Input.php
index 0faab2cf1..d2881c60f 100644
--- a/Input/Input.php
+++ b/Input/Input.php
@@ -27,11 +27,12 @@
*/
abstract class Input implements InputInterface, StreamableInputInterface
{
- protected $definition;
+ protected InputDefinition $definition;
+ /** @var resource */
protected $stream;
- protected $options = [];
- protected $arguments = [];
- protected $interactive = true;
+ protected array $options = [];
+ protected array $arguments = [];
+ protected bool $interactive = true;
public function __construct(?InputDefinition $definition = null)
{
@@ -43,10 +44,7 @@ public function __construct(?InputDefinition $definition = null)
}
}
- /**
- * {@inheritdoc}
- */
- public function bind(InputDefinition $definition)
+ public function bind(InputDefinition $definition): void
{
$this->arguments = [];
$this->options = [];
@@ -58,93 +56,64 @@ public function bind(InputDefinition $definition)
/**
* Processes command line arguments.
*/
- abstract protected function parse();
+ abstract protected function parse(): void;
- /**
- * {@inheritdoc}
- */
- public function validate()
+ public function validate(): void
{
$definition = $this->definition;
$givenArguments = $this->arguments;
- $missingArguments = array_filter(array_keys($definition->getArguments()), function ($argument) use ($definition, $givenArguments) {
- return !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired();
- });
+ $missingArguments = array_filter(array_keys($definition->getArguments()), fn ($argument) => !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired());
if (\count($missingArguments) > 0) {
- throw new RuntimeException(sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments)));
+ throw new RuntimeException(\sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments)));
}
}
- /**
- * {@inheritdoc}
- */
- public function isInteractive()
+ public function isInteractive(): bool
{
return $this->interactive;
}
- /**
- * {@inheritdoc}
- */
- public function setInteractive(bool $interactive)
+ public function setInteractive(bool $interactive): void
{
$this->interactive = $interactive;
}
- /**
- * {@inheritdoc}
- */
- public function getArguments()
+ public function getArguments(): array
{
return array_merge($this->definition->getArgumentDefaults(), $this->arguments);
}
- /**
- * {@inheritdoc}
- */
- public function getArgument(string $name)
+ public function getArgument(string $name): mixed
{
if (!$this->definition->hasArgument($name)) {
- throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+ throw new InvalidArgumentException(\sprintf('The "%s" argument does not exist.', $name));
}
return $this->arguments[$name] ?? $this->definition->getArgument($name)->getDefault();
}
- /**
- * {@inheritdoc}
- */
- public function setArgument(string $name, $value)
+ public function setArgument(string $name, mixed $value): void
{
if (!$this->definition->hasArgument($name)) {
- throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+ throw new InvalidArgumentException(\sprintf('The "%s" argument does not exist.', $name));
}
$this->arguments[$name] = $value;
}
- /**
- * {@inheritdoc}
- */
- public function hasArgument(string $name)
+ public function hasArgument(string $name): bool
{
return $this->definition->hasArgument($name);
}
- /**
- * {@inheritdoc}
- */
- public function getOptions()
+ public function getOptions(): array
{
return array_merge($this->definition->getOptionDefaults(), $this->options);
}
- /**
- * {@inheritdoc}
- */
- public function getOption(string $name)
+ public function getOption(string $name): mixed
{
if ($this->definition->hasNegation($name)) {
if (null === $value = $this->getOption($this->definition->negationToName($name))) {
@@ -155,56 +124,48 @@ public function getOption(string $name)
}
if (!$this->definition->hasOption($name)) {
- throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
+ throw new InvalidArgumentException(\sprintf('The "%s" option does not exist.', $name));
}
return \array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault();
}
- /**
- * {@inheritdoc}
- */
- public function setOption(string $name, $value)
+ public function setOption(string $name, mixed $value): void
{
if ($this->definition->hasNegation($name)) {
$this->options[$this->definition->negationToName($name)] = !$value;
return;
} elseif (!$this->definition->hasOption($name)) {
- throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
+ throw new InvalidArgumentException(\sprintf('The "%s" option does not exist.', $name));
}
$this->options[$name] = $value;
}
- /**
- * {@inheritdoc}
- */
- public function hasOption(string $name)
+ public function hasOption(string $name): bool
{
return $this->definition->hasOption($name) || $this->definition->hasNegation($name);
}
/**
* Escapes a token through escapeshellarg if it contains unsafe chars.
- *
- * @return string
*/
- public function escapeToken(string $token)
+ public function escapeToken(string $token): string
{
return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token);
}
/**
- * {@inheritdoc}
+ * @param resource $stream
*/
- public function setStream($stream)
+ public function setStream($stream): void
{
$this->stream = $stream;
}
/**
- * {@inheritdoc}
+ * @return resource
*/
public function getStream()
{
diff --git a/Input/InputArgument.php b/Input/InputArgument.php
index 1a8bf44b7..6fbb64ed0 100644
--- a/Input/InputArgument.php
+++ b/Input/InputArgument.php
@@ -11,6 +11,10 @@
namespace Symfony\Component\Console\Input;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Completion\CompletionInput;
+use Symfony\Component\Console\Completion\CompletionSuggestions;
+use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
@@ -21,44 +25,55 @@
*/
class InputArgument
{
+ /**
+ * Providing an argument is required (e.g. just 'app:foo' is not allowed).
+ */
public const REQUIRED = 1;
+
+ /**
+ * Providing an argument is optional (e.g. 'app:foo' and 'app:foo bar' are both allowed). This is the default behavior of arguments.
+ */
public const OPTIONAL = 2;
+
+ /**
+ * The argument accepts multiple values and turn them into an array (e.g. 'app:foo bar baz' will result in value ['bar', 'baz']).
+ */
public const IS_ARRAY = 4;
- private $name;
- private $mode;
- private $default;
- private $description;
+ private int $mode;
+ private string|int|bool|array|float|null $default;
/**
- * @param string $name The argument name
- * @param int|null $mode The argument mode: a bit mask of self::REQUIRED, self::OPTIONAL and self::IS_ARRAY
- * @param string $description A description text
- * @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only)
+ * @param string $name The argument name
+ * @param int-mask-of|null $mode The argument mode: a bit mask of self::REQUIRED, self::OPTIONAL and self::IS_ARRAY
+ * @param string $description A description text
+ * @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only)
+ * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion
*
* @throws InvalidArgumentException When argument mode is not valid
*/
- public function __construct(string $name, ?int $mode = null, string $description = '', $default = null)
- {
+ public function __construct(
+ private string $name,
+ ?int $mode = null,
+ private string $description = '',
+ string|bool|int|float|array|null $default = null,
+ private \Closure|array $suggestedValues = [],
+ ) {
if (null === $mode) {
$mode = self::OPTIONAL;
- } elseif ($mode > 7 || $mode < 1) {
- throw new InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode));
+ } elseif ($mode >= (self::IS_ARRAY << 1) || $mode < 1) {
+ throw new InvalidArgumentException(\sprintf('Argument mode "%s" is not valid.', $mode));
}
- $this->name = $name;
$this->mode = $mode;
- $this->description = $description;
$this->setDefault($default);
}
/**
* Returns the argument name.
- *
- * @return string
*/
- public function getName()
+ public function getName(): string
{
return $this->name;
}
@@ -68,7 +83,7 @@ public function getName()
*
* @return bool true if parameter mode is self::REQUIRED, false otherwise
*/
- public function isRequired()
+ public function isRequired(): bool
{
return self::REQUIRED === (self::REQUIRED & $this->mode);
}
@@ -78,19 +93,15 @@ public function isRequired()
*
* @return bool true if mode is self::IS_ARRAY, false otherwise
*/
- public function isArray()
+ public function isArray(): bool
{
return self::IS_ARRAY === (self::IS_ARRAY & $this->mode);
}
/**
* Sets the default value.
- *
- * @param string|bool|int|float|array|null $default
- *
- * @throws LogicException When incorrect default value is given
*/
- public function setDefault($default = null)
+ public function setDefault(string|bool|int|float|array|null $default): void
{
if ($this->isRequired() && null !== $default) {
throw new LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.');
@@ -109,20 +120,40 @@ public function setDefault($default = null)
/**
* Returns the default value.
- *
- * @return string|bool|int|float|array|null
*/
- public function getDefault()
+ public function getDefault(): string|bool|int|float|array|null
{
return $this->default;
}
/**
- * Returns the description text.
+ * Returns true if the argument has values for input completion.
+ */
+ public function hasCompletion(): bool
+ {
+ return [] !== $this->suggestedValues;
+ }
+
+ /**
+ * Supplies suggestions when command resolves possible completion options for input.
*
- * @return string
+ * @see Command::complete()
+ */
+ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
+ {
+ $values = $this->suggestedValues;
+ if ($values instanceof \Closure && !\is_array($values = $values($input))) {
+ throw new LogicException(\sprintf('Closure for argument "%s" must return an array. Got "%s".', $this->name, get_debug_type($values)));
+ }
+ if ($values) {
+ $suggestions->suggestValues($values);
+ }
+ }
+
+ /**
+ * Returns the description text.
*/
- public function getDescription()
+ public function getDescription(): string
{
return $this->description;
}
diff --git a/Input/InputAwareInterface.php b/Input/InputAwareInterface.php
index 5a288de5d..ba4664cdb 100644
--- a/Input/InputAwareInterface.php
+++ b/Input/InputAwareInterface.php
@@ -22,5 +22,5 @@ interface InputAwareInterface
/**
* Sets the Console Input.
*/
- public function setInput(InputInterface $input);
+ public function setInput(InputInterface $input): void;
}
diff --git a/Input/InputDefinition.php b/Input/InputDefinition.php
index 11f704f0e..a8b006d48 100644
--- a/Input/InputDefinition.php
+++ b/Input/InputDefinition.php
@@ -28,13 +28,13 @@
*/
class InputDefinition
{
- private $arguments;
- private $requiredCount;
- private $lastArrayArgument;
- private $lastOptionalArgument;
- private $options;
- private $negations;
- private $shortcuts;
+ private array $arguments = [];
+ private int $requiredCount = 0;
+ private ?InputArgument $lastArrayArgument = null;
+ private ?InputArgument $lastOptionalArgument = null;
+ private array $options = [];
+ private array $negations = [];
+ private array $shortcuts = [];
/**
* @param array $definition An array of InputArgument and InputOption instance
@@ -47,7 +47,7 @@ public function __construct(array $definition = [])
/**
* Sets the definition of the input.
*/
- public function setDefinition(array $definition)
+ public function setDefinition(array $definition): void
{
$arguments = [];
$options = [];
@@ -68,7 +68,7 @@ public function setDefinition(array $definition)
*
* @param InputArgument[] $arguments An array of InputArgument objects
*/
- public function setArguments(array $arguments = [])
+ public function setArguments(array $arguments = []): void
{
$this->arguments = [];
$this->requiredCount = 0;
@@ -82,7 +82,7 @@ public function setArguments(array $arguments = [])
*
* @param InputArgument[] $arguments An array of InputArgument objects
*/
- public function addArguments(?array $arguments = [])
+ public function addArguments(?array $arguments = []): void
{
if (null !== $arguments) {
foreach ($arguments as $argument) {
@@ -94,18 +94,18 @@ public function addArguments(?array $arguments = [])
/**
* @throws LogicException When incorrect argument is given
*/
- public function addArgument(InputArgument $argument)
+ public function addArgument(InputArgument $argument): void
{
if (isset($this->arguments[$argument->getName()])) {
- throw new LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName()));
+ throw new LogicException(\sprintf('An argument with name "%s" already exists.', $argument->getName()));
}
if (null !== $this->lastArrayArgument) {
- throw new LogicException(sprintf('Cannot add a required argument "%s" after an array argument "%s".', $argument->getName(), $this->lastArrayArgument->getName()));
+ throw new LogicException(\sprintf('Cannot add a required argument "%s" after an array argument "%s".', $argument->getName(), $this->lastArrayArgument->getName()));
}
if ($argument->isRequired() && null !== $this->lastOptionalArgument) {
- throw new LogicException(sprintf('Cannot add a required argument "%s" after an optional one "%s".', $argument->getName(), $this->lastOptionalArgument->getName()));
+ throw new LogicException(\sprintf('Cannot add a required argument "%s" after an optional one "%s".', $argument->getName(), $this->lastOptionalArgument->getName()));
}
if ($argument->isArray()) {
@@ -124,16 +124,12 @@ public function addArgument(InputArgument $argument)
/**
* Returns an InputArgument by name or by position.
*
- * @param string|int $name The InputArgument name or position
- *
- * @return InputArgument
- *
* @throws InvalidArgumentException When argument given doesn't exist
*/
- public function getArgument($name)
+ public function getArgument(string|int $name): InputArgument
{
if (!$this->hasArgument($name)) {
- throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
+ throw new InvalidArgumentException(\sprintf('The "%s" argument does not exist.', $name));
}
$arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments;
@@ -143,12 +139,8 @@ public function getArgument($name)
/**
* Returns true if an InputArgument object exists by name or position.
- *
- * @param string|int $name The InputArgument name or position
- *
- * @return bool
*/
- public function hasArgument($name)
+ public function hasArgument(string|int $name): bool
{
$arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments;
@@ -160,27 +152,23 @@ public function hasArgument($name)
*
* @return InputArgument[]
*/
- public function getArguments()
+ public function getArguments(): array
{
return $this->arguments;
}
/**
* Returns the number of InputArguments.
- *
- * @return int
*/
- public function getArgumentCount()
+ public function getArgumentCount(): int
{
return null !== $this->lastArrayArgument ? \PHP_INT_MAX : \count($this->arguments);
}
/**
* Returns the number of required InputArguments.
- *
- * @return int
*/
- public function getArgumentRequiredCount()
+ public function getArgumentRequiredCount(): int
{
return $this->requiredCount;
}
@@ -188,7 +176,7 @@ public function getArgumentRequiredCount()
/**
* @return array
*/
- public function getArgumentDefaults()
+ public function getArgumentDefaults(): array
{
$values = [];
foreach ($this->arguments as $argument) {
@@ -203,7 +191,7 @@ public function getArgumentDefaults()
*
* @param InputOption[] $options An array of InputOption objects
*/
- public function setOptions(array $options = [])
+ public function setOptions(array $options = []): void
{
$this->options = [];
$this->shortcuts = [];
@@ -216,7 +204,7 @@ public function setOptions(array $options = [])
*
* @param InputOption[] $options An array of InputOption objects
*/
- public function addOptions(array $options = [])
+ public function addOptions(array $options = []): void
{
foreach ($options as $option) {
$this->addOption($option);
@@ -226,19 +214,19 @@ public function addOptions(array $options = [])
/**
* @throws LogicException When option given already exist
*/
- public function addOption(InputOption $option)
+ public function addOption(InputOption $option): void
{
if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) {
- throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName()));
+ throw new LogicException(\sprintf('An option named "%s" already exists.', $option->getName()));
}
if (isset($this->negations[$option->getName()])) {
- throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName()));
+ throw new LogicException(\sprintf('An option named "%s" already exists.', $option->getName()));
}
if ($option->getShortcut()) {
foreach (explode('|', $option->getShortcut()) as $shortcut) {
if (isset($this->shortcuts[$shortcut]) && !$option->equals($this->options[$this->shortcuts[$shortcut]])) {
- throw new LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut));
+ throw new LogicException(\sprintf('An option with shortcut "%s" already exists.', $shortcut));
}
}
}
@@ -253,7 +241,7 @@ public function addOption(InputOption $option)
if ($option->isNegatable()) {
$negatedName = 'no-'.$option->getName();
if (isset($this->options[$negatedName])) {
- throw new LogicException(sprintf('An option named "%s" already exists.', $negatedName));
+ throw new LogicException(\sprintf('An option named "%s" already exists.', $negatedName));
}
$this->negations[$negatedName] = $option->getName();
}
@@ -262,14 +250,12 @@ public function addOption(InputOption $option)
/**
* Returns an InputOption by name.
*
- * @return InputOption
- *
* @throws InvalidArgumentException When option given doesn't exist
*/
- public function getOption(string $name)
+ public function getOption(string $name): InputOption
{
if (!$this->hasOption($name)) {
- throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name));
+ throw new InvalidArgumentException(\sprintf('The "--%s" option does not exist.', $name));
}
return $this->options[$name];
@@ -280,10 +266,8 @@ public function getOption(string $name)
*
* This method can't be used to check if the user included the option when
* executing the command (use getOption() instead).
- *
- * @return bool
*/
- public function hasOption(string $name)
+ public function hasOption(string $name): bool
{
return isset($this->options[$name]);
}
@@ -293,17 +277,15 @@ public function hasOption(string $name)
*
* @return InputOption[]
*/
- public function getOptions()
+ public function getOptions(): array
{
return $this->options;
}
/**
* Returns true if an InputOption object exists by shortcut.
- *
- * @return bool
*/
- public function hasShortcut(string $name)
+ public function hasShortcut(string $name): bool
{
return isset($this->shortcuts[$name]);
}
@@ -318,10 +300,8 @@ public function hasNegation(string $name): bool
/**
* Gets an InputOption by shortcut.
- *
- * @return InputOption
*/
- public function getOptionForShortcut(string $shortcut)
+ public function getOptionForShortcut(string $shortcut): InputOption
{
return $this->getOption($this->shortcutToName($shortcut));
}
@@ -329,7 +309,7 @@ public function getOptionForShortcut(string $shortcut)
/**
* @return array
*/
- public function getOptionDefaults()
+ public function getOptionDefaults(): array
{
$values = [];
foreach ($this->options as $option) {
@@ -349,7 +329,7 @@ public function getOptionDefaults()
public function shortcutToName(string $shortcut): string
{
if (!isset($this->shortcuts[$shortcut])) {
- throw new InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut));
+ throw new InvalidArgumentException(\sprintf('The "-%s" option does not exist.', $shortcut));
}
return $this->shortcuts[$shortcut];
@@ -365,7 +345,7 @@ public function shortcutToName(string $shortcut): string
public function negationToName(string $negation): string
{
if (!isset($this->negations[$negation])) {
- throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $negation));
+ throw new InvalidArgumentException(\sprintf('The "--%s" option does not exist.', $negation));
}
return $this->negations[$negation];
@@ -373,10 +353,8 @@ public function negationToName(string $negation): string
/**
* Gets the synopsis.
- *
- * @return string
*/
- public function getSynopsis(bool $short = false)
+ public function getSynopsis(bool $short = false): string
{
$elements = [];
@@ -386,7 +364,7 @@ public function getSynopsis(bool $short = false)
foreach ($this->getOptions() as $option) {
$value = '';
if ($option->acceptValue()) {
- $value = sprintf(
+ $value = \sprintf(
' %s%s%s',
$option->isValueOptional() ? '[' : '',
strtoupper($option->getName()),
@@ -394,9 +372,9 @@ public function getSynopsis(bool $short = false)
);
}
- $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : '';
- $negation = $option->isNegatable() ? sprintf('|--no-%s', $option->getName()) : '';
- $elements[] = sprintf('[%s--%s%s%s]', $shortcut, $option->getName(), $value, $negation);
+ $shortcut = $option->getShortcut() ? \sprintf('-%s|', $option->getShortcut()) : '';
+ $negation = $option->isNegatable() ? \sprintf('|--no-%s', $option->getName()) : '';
+ $elements[] = \sprintf('[%s--%s%s%s]', $shortcut, $option->getName(), $value, $negation);
}
}
diff --git a/Input/InputInterface.php b/Input/InputInterface.php
index 628b6037a..c177d960b 100644
--- a/Input/InputInterface.php
+++ b/Input/InputInterface.php
@@ -23,10 +23,8 @@ interface InputInterface
{
/**
* Returns the first argument from the raw parameters (not parsed).
- *
- * @return string|null
*/
- public function getFirstArgument();
+ public function getFirstArgument(): ?string;
/**
* Returns true if the raw parameters (not parsed) contain a value.
@@ -38,10 +36,8 @@ public function getFirstArgument();
*
* @param string|array $values The values to look for in the raw parameters (can be an array)
* @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal
- *
- * @return bool
*/
- public function hasParameterOption($values, bool $onlyParams = false);
+ public function hasParameterOption(string|array $values, bool $onlyParams = false): bool;
/**
* Returns the value of a raw option (not parsed).
@@ -54,98 +50,89 @@ public function hasParameterOption($values, bool $onlyParams = false);
* @param string|array $values The value(s) to look for in the raw parameters (can be an array)
* @param string|bool|int|float|array|null $default The default value to return if no result is found
* @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal
- *
- * @return mixed
*/
- public function getParameterOption($values, $default = false, bool $onlyParams = false);
+ public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed;
/**
* Binds the current Input instance with the given arguments and options.
*
* @throws RuntimeException
*/
- public function bind(InputDefinition $definition);
+ public function bind(InputDefinition $definition): void;
/**
* Validates the input.
*
* @throws RuntimeException When not enough arguments are given
*/
- public function validate();
+ public function validate(): void;
/**
* Returns all the given arguments merged with the default values.
*
* @return array
*/
- public function getArguments();
+ public function getArguments(): array;
/**
* Returns the argument value for a given argument name.
*
- * @return mixed
- *
* @throws InvalidArgumentException When argument given doesn't exist
*/
- public function getArgument(string $name);
+ public function getArgument(string $name): mixed;
/**
* Sets an argument value by name.
*
- * @param mixed $value The argument value
- *
* @throws InvalidArgumentException When argument given doesn't exist
*/
- public function setArgument(string $name, $value);
+ public function setArgument(string $name, mixed $value): void;
/**
* Returns true if an InputArgument object exists by name or position.
- *
- * @return bool
*/
- public function hasArgument(string $name);
+ public function hasArgument(string $name): bool;
/**
* Returns all the given options merged with the default values.
*
* @return array
*/
- public function getOptions();
+ public function getOptions(): array;
/**
* Returns the option value for a given option name.
*
- * @return mixed
- *
* @throws InvalidArgumentException When option given doesn't exist
*/
- public function getOption(string $name);
+ public function getOption(string $name): mixed;
/**
* Sets an option value by name.
*
- * @param mixed $value The option value
- *
* @throws InvalidArgumentException When option given doesn't exist
*/
- public function setOption(string $name, $value);
+ public function setOption(string $name, mixed $value): void;
/**
* Returns true if an InputOption object exists by name.
- *
- * @return bool
*/
- public function hasOption(string $name);
+ public function hasOption(string $name): bool;
/**
* Is this input means interactive?
- *
- * @return bool
*/
- public function isInteractive();
+ public function isInteractive(): bool;
/**
* Sets the input interactivity.
*/
- public function setInteractive(bool $interactive);
+ public function setInteractive(bool $interactive): void;
+
+ /**
+ * Returns a stringified representation of the args passed to the command.
+ *
+ * InputArguments MUST be escaped as well as the InputOption values passed to the command.
+ */
+ public function __toString(): string;
}
diff --git a/Input/InputOption.php b/Input/InputOption.php
index 99807f59e..25fb91782 100644
--- a/Input/InputOption.php
+++ b/Input/InputOption.php
@@ -11,6 +11,10 @@
namespace Symfony\Component\Console\Input;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Completion\CompletionInput;
+use Symfony\Component\Console\Completion\CompletionSuggestions;
+use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
@@ -42,30 +46,36 @@ class InputOption
public const VALUE_IS_ARRAY = 8;
/**
- * The option may have either positive or negative value (e.g. --ansi or --no-ansi).
+ * The option allows passing a negated variant (e.g. --ansi or --no-ansi).
*/
public const VALUE_NEGATABLE = 16;
- private $name;
- private $shortcut;
- private $mode;
- private $default;
- private $description;
+ private string $name;
+ private ?string $shortcut;
+ private int $mode;
+ private string|int|bool|array|float|null $default;
/**
- * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
- * @param int|null $mode The option mode: One of the VALUE_* constants
- * @param string|bool|int|float|array|null $default The default value (must be null for self::VALUE_NONE)
+ * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
+ * @param int-mask-of|null $mode The option mode: One of the VALUE_* constants
+ * @param string|bool|int|float|array|null $default The default value (must be null for self::VALUE_NONE)
+ * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion
*
* @throws InvalidArgumentException If option mode is invalid or incompatible
*/
- public function __construct(string $name, $shortcut = null, ?int $mode = null, string $description = '', $default = null)
- {
+ public function __construct(
+ string $name,
+ string|array|null $shortcut = null,
+ ?int $mode = null,
+ private string $description = '',
+ string|bool|int|float|array|null $default = null,
+ private array|\Closure $suggestedValues = [],
+ ) {
if (str_starts_with($name, '--')) {
$name = substr($name, 2);
}
- if (empty($name)) {
+ if (!$name) {
throw new InvalidArgumentException('An option name cannot be empty.');
}
@@ -89,14 +99,16 @@ public function __construct(string $name, $shortcut = null, ?int $mode = null, s
if (null === $mode) {
$mode = self::VALUE_NONE;
} elseif ($mode >= (self::VALUE_NEGATABLE << 1) || $mode < 1) {
- throw new InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode));
+ throw new InvalidArgumentException(\sprintf('Option mode "%s" is not valid.', $mode));
}
$this->name = $name;
$this->shortcut = $shortcut;
$this->mode = $mode;
- $this->description = $description;
+ if ($suggestedValues && !$this->acceptValue()) {
+ throw new LogicException('Cannot set suggested values if the option does not accept a value.');
+ }
if ($this->isArray() && !$this->acceptValue()) {
throw new InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.');
}
@@ -109,20 +121,16 @@ public function __construct(string $name, $shortcut = null, ?int $mode = null, s
/**
* Returns the option shortcut.
- *
- * @return string|null
*/
- public function getShortcut()
+ public function getShortcut(): ?string
{
return $this->shortcut;
}
/**
* Returns the option name.
- *
- * @return string
*/
- public function getName()
+ public function getName(): string
{
return $this->name;
}
@@ -132,7 +140,7 @@ public function getName()
*
* @return bool true if value mode is not self::VALUE_NONE, false otherwise
*/
- public function acceptValue()
+ public function acceptValue(): bool
{
return $this->isValueRequired() || $this->isValueOptional();
}
@@ -142,7 +150,7 @@ public function acceptValue()
*
* @return bool true if value mode is self::VALUE_REQUIRED, false otherwise
*/
- public function isValueRequired()
+ public function isValueRequired(): bool
{
return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode);
}
@@ -152,7 +160,7 @@ public function isValueRequired()
*
* @return bool true if value mode is self::VALUE_OPTIONAL, false otherwise
*/
- public function isValueOptional()
+ public function isValueOptional(): bool
{
return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode);
}
@@ -162,20 +170,25 @@ public function isValueOptional()
*
* @return bool true if mode is self::VALUE_IS_ARRAY, false otherwise
*/
- public function isArray()
+ public function isArray(): bool
{
return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode);
}
+ /**
+ * Returns true if the option allows passing a negated variant.
+ *
+ * @return bool true if mode is self::VALUE_NEGATABLE, false otherwise
+ */
public function isNegatable(): bool
{
return self::VALUE_NEGATABLE === (self::VALUE_NEGATABLE & $this->mode);
}
/**
- * @param string|bool|int|float|array|null $default
+ * Sets the default value.
*/
- public function setDefault($default = null)
+ public function setDefault(string|bool|int|float|array|null $default): void
{
if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) {
throw new LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.');
@@ -194,30 +207,48 @@ public function setDefault($default = null)
/**
* Returns the default value.
- *
- * @return string|bool|int|float|array|null
*/
- public function getDefault()
+ public function getDefault(): string|bool|int|float|array|null
{
return $this->default;
}
/**
* Returns the description text.
- *
- * @return string
*/
- public function getDescription()
+ public function getDescription(): string
{
return $this->description;
}
/**
- * Checks whether the given option equals this one.
+ * Returns true if the option has values for input completion.
+ */
+ public function hasCompletion(): bool
+ {
+ return [] !== $this->suggestedValues;
+ }
+
+ /**
+ * Supplies suggestions when command resolves possible completion options for input.
*
- * @return bool
+ * @see Command::complete()
+ */
+ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
+ {
+ $values = $this->suggestedValues;
+ if ($values instanceof \Closure && !\is_array($values = $values($input))) {
+ throw new LogicException(\sprintf('Closure for option "%s" must return an array. Got "%s".', $this->name, get_debug_type($values)));
+ }
+ if ($values) {
+ $suggestions->suggestValues($values);
+ }
+ }
+
+ /**
+ * Checks whether the given option equals this one.
*/
- public function equals(self $option)
+ public function equals(self $option): bool
{
return $option->getName() === $this->getName()
&& $option->getShortcut() === $this->getShortcut()
diff --git a/Input/StreamableInputInterface.php b/Input/StreamableInputInterface.php
index d7e462f24..4a0dc017f 100644
--- a/Input/StreamableInputInterface.php
+++ b/Input/StreamableInputInterface.php
@@ -26,7 +26,7 @@ interface StreamableInputInterface extends InputInterface
*
* @param resource $stream The input stream
*/
- public function setStream($stream);
+ public function setStream($stream): void;
/**
* Returns the input stream.
diff --git a/Input/StringInput.php b/Input/StringInput.php
index 56bb66cbf..a70f048f9 100644
--- a/Input/StringInput.php
+++ b/Input/StringInput.php
@@ -24,7 +24,6 @@
*/
class StringInput extends ArgvInput
{
- public const REGEX_STRING = '([^\s]+?)(?:\s|(?
+ *
* @throws InvalidArgumentException When unable to parse input (should never happen)
*/
private function tokenize(string $input): array
@@ -69,7 +70,7 @@ private function tokenize(string $input): array
$token .= $match[1];
} else {
// should never happen
- throw new InvalidArgumentException(sprintf('Unable to parse input near "... %s ...".', substr($input, $cursor, 10)));
+ throw new InvalidArgumentException(\sprintf('Unable to parse input near "... %s ...".', substr($input, $cursor, 10)));
}
$cursor += \strlen($match[0]);
diff --git a/Logger/ConsoleLogger.php b/Logger/ConsoleLogger.php
index 4a10fa172..a6ef49ea9 100644
--- a/Logger/ConsoleLogger.php
+++ b/Logger/ConsoleLogger.php
@@ -29,8 +29,7 @@ class ConsoleLogger extends AbstractLogger
public const INFO = 'info';
public const ERROR = 'error';
- private $output;
- private $verbosityLevelMap = [
+ private array $verbosityLevelMap = [
LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL,
LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL,
LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL,
@@ -40,7 +39,7 @@ class ConsoleLogger extends AbstractLogger
LogLevel::INFO => OutputInterface::VERBOSITY_VERY_VERBOSE,
LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG,
];
- private $formatLevelMap = [
+ private array $formatLevelMap = [
LogLevel::EMERGENCY => self::ERROR,
LogLevel::ALERT => self::ERROR,
LogLevel::CRITICAL => self::ERROR,
@@ -50,24 +49,21 @@ class ConsoleLogger extends AbstractLogger
LogLevel::INFO => self::INFO,
LogLevel::DEBUG => self::INFO,
];
- private $errored = false;
+ private bool $errored = false;
- public function __construct(OutputInterface $output, array $verbosityLevelMap = [], array $formatLevelMap = [])
- {
- $this->output = $output;
+ public function __construct(
+ private OutputInterface $output,
+ array $verbosityLevelMap = [],
+ array $formatLevelMap = [],
+ ) {
$this->verbosityLevelMap = $verbosityLevelMap + $this->verbosityLevelMap;
$this->formatLevelMap = $formatLevelMap + $this->formatLevelMap;
}
- /**
- * {@inheritdoc}
- *
- * @return void
- */
- public function log($level, $message, array $context = [])
+ public function log($level, $message, array $context = []): void
{
if (!isset($this->verbosityLevelMap[$level])) {
- throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level));
+ throw new InvalidArgumentException(\sprintf('The log level "%s" does not exist.', $level));
}
$output = $this->output;
@@ -83,16 +79,14 @@ public function log($level, $message, array $context = [])
// the if condition check isn't necessary -- it's the same one that $output will do internally anyway.
// We only do it for efficiency here as the message formatting is relatively expensive.
if ($output->getVerbosity() >= $this->verbosityLevelMap[$level]) {
- $output->writeln(sprintf('<%1$s>[%2$s] %3$s%1$s>', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context)), $this->verbosityLevelMap[$level]);
+ $output->writeln(\sprintf('<%1$s>[%2$s] %3$s%1$s>', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context)), $this->verbosityLevelMap[$level]);
}
}
/**
* Returns true when any messages have been logged at error levels.
- *
- * @return bool
*/
- public function hasErrored()
+ public function hasErrored(): bool
{
return $this->errored;
}
@@ -110,12 +104,12 @@ private function interpolate(string $message, array $context): string
$replacements = [];
foreach ($context as $key => $val) {
- if (null === $val || \is_scalar($val) || (\is_object($val) && method_exists($val, '__toString'))) {
+ if (null === $val || \is_scalar($val) || $val instanceof \Stringable) {
$replacements["{{$key}}"] = $val;
} elseif ($val instanceof \DateTimeInterface) {
- $replacements["{{$key}}"] = $val->format(\DateTime::RFC3339);
+ $replacements["{{$key}}"] = $val->format(\DateTimeInterface::RFC3339);
} elseif (\is_object($val)) {
- $replacements["{{$key}}"] = '[object '.\get_class($val).']';
+ $replacements["{{$key}}"] = '[object '.$val::class.']';
} else {
$replacements["{{$key}}"] = '['.\gettype($val).']';
}
diff --git a/Messenger/RunCommandContext.php b/Messenger/RunCommandContext.php
new file mode 100644
index 000000000..2ee5415c6
--- /dev/null
+++ b/Messenger/RunCommandContext.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Messenger;
+
+/**
+ * @author Kevin Bond
+ */
+final class RunCommandContext
+{
+ public function __construct(
+ public readonly RunCommandMessage $message,
+ public readonly int $exitCode,
+ public readonly string $output,
+ ) {
+ }
+}
diff --git a/Messenger/RunCommandMessage.php b/Messenger/RunCommandMessage.php
new file mode 100644
index 000000000..b530c438c
--- /dev/null
+++ b/Messenger/RunCommandMessage.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Messenger;
+
+use Symfony\Component\Console\Exception\RunCommandFailedException;
+
+/**
+ * @author Kevin Bond
+ */
+class RunCommandMessage implements \Stringable
+{
+ /**
+ * @param bool $throwOnFailure If the command has a non-zero exit code, throw {@see RunCommandFailedException}
+ * @param bool $catchExceptions @see Application::setCatchExceptions()
+ */
+ public function __construct(
+ public readonly string $input,
+ public readonly bool $throwOnFailure = true,
+ public readonly bool $catchExceptions = false,
+ ) {
+ }
+
+ public function __toString(): string
+ {
+ return $this->input;
+ }
+}
diff --git a/Messenger/RunCommandMessageHandler.php b/Messenger/RunCommandMessageHandler.php
new file mode 100644
index 000000000..0fdf7d017
--- /dev/null
+++ b/Messenger/RunCommandMessageHandler.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Messenger;
+
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Exception\RunCommandFailedException;
+use Symfony\Component\Console\Input\StringInput;
+use Symfony\Component\Console\Output\BufferedOutput;
+
+/**
+ * @author Kevin Bond
+ */
+final class RunCommandMessageHandler
+{
+ public function __construct(
+ private readonly Application $application,
+ ) {
+ }
+
+ public function __invoke(RunCommandMessage $message): RunCommandContext
+ {
+ $input = new StringInput($message->input);
+ $output = new BufferedOutput();
+
+ $this->application->setCatchExceptions($message->catchExceptions);
+
+ try {
+ $exitCode = $this->application->run($input, $output);
+ } catch (\Throwable $e) {
+ throw new RunCommandFailedException($e, new RunCommandContext($message, Command::FAILURE, $output->fetch()));
+ }
+
+ if ($message->throwOnFailure && Command::SUCCESS !== $exitCode) {
+ throw new RunCommandFailedException(\sprintf('Command "%s" exited with code "%s".', $message->input, $exitCode), new RunCommandContext($message, $exitCode, $output->fetch()));
+ }
+
+ return new RunCommandContext($message, $exitCode, $output->fetch());
+ }
+}
diff --git a/Output/AnsiColorMode.php b/Output/AnsiColorMode.php
new file mode 100644
index 000000000..0e1422a27
--- /dev/null
+++ b/Output/AnsiColorMode.php
@@ -0,0 +1,106 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Output;
+
+use Symfony\Component\Console\Exception\InvalidArgumentException;
+
+/**
+ * @author Fabien Potencier
+ * @author Julien Boudry
+ */
+enum AnsiColorMode
+{
+ /*
+ * Classical 4-bit Ansi colors, including 8 classical colors and 8 bright color. Output syntax is "ESC[${foreGroundColorcode};${backGroundColorcode}m"
+ * Must be compatible with all terminals and it's the minimal version supported.
+ */
+ case Ansi4;
+
+ /*
+ * 8-bit Ansi colors (240 different colors + 16 duplicate color codes, ensuring backward compatibility).
+ * Output syntax is: "ESC[38;5;${foreGroundColorcode};48;5;${backGroundColorcode}m"
+ * Should be compatible with most terminals.
+ */
+ case Ansi8;
+
+ /*
+ * 24-bit Ansi colors (RGB).
+ * Output syntax is: "ESC[38;2;${foreGroundColorcodeRed};${foreGroundColorcodeGreen};${foreGroundColorcodeBlue};48;2;${backGroundColorcodeRed};${backGroundColorcodeGreen};${backGroundColorcodeBlue}m"
+ * May be compatible with many modern terminals.
+ */
+ case Ansi24;
+
+ /**
+ * Converts an RGB hexadecimal color to the corresponding Ansi code.
+ */
+ public function convertFromHexToAnsiColorCode(string $hexColor): string
+ {
+ $hexColor = str_replace('#', '', $hexColor);
+
+ if (3 === \strlen($hexColor)) {
+ $hexColor = $hexColor[0].$hexColor[0].$hexColor[1].$hexColor[1].$hexColor[2].$hexColor[2];
+ }
+
+ if (6 !== \strlen($hexColor)) {
+ throw new InvalidArgumentException(\sprintf('Invalid "#%s" color.', $hexColor));
+ }
+
+ $color = hexdec($hexColor);
+
+ $r = ($color >> 16) & 255;
+ $g = ($color >> 8) & 255;
+ $b = $color & 255;
+
+ return match ($this) {
+ self::Ansi4 => (string) $this->convertFromRGB($r, $g, $b),
+ self::Ansi8 => '8;5;'.$this->convertFromRGB($r, $g, $b),
+ self::Ansi24 => \sprintf('8;2;%d;%d;%d', $r, $g, $b),
+ };
+ }
+
+ private function convertFromRGB(int $r, int $g, int $b): int
+ {
+ return match ($this) {
+ self::Ansi4 => $this->degradeHexColorToAnsi4($r, $g, $b),
+ self::Ansi8 => $this->degradeHexColorToAnsi8($r, $g, $b),
+ default => throw new InvalidArgumentException("RGB cannot be converted to {$this->name}."),
+ };
+ }
+
+ private function degradeHexColorToAnsi4(int $r, int $g, int $b): int
+ {
+ return round($b / 255) << 2 | (round($g / 255) << 1) | round($r / 255);
+ }
+
+ /**
+ * Inspired from https://github.com/ajalt/colormath/blob/e464e0da1b014976736cf97250063248fc77b8e7/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Ansi256.kt code (MIT license).
+ */
+ private function degradeHexColorToAnsi8(int $r, int $g, int $b): int
+ {
+ if ($r === $g && $g === $b) {
+ if ($r < 8) {
+ return 16;
+ }
+
+ if ($r > 248) {
+ return 231;
+ }
+
+ return (int) round(($r - 8) / 247 * 24) + 232;
+ }
+
+ return 16 +
+ (36 * (int) round($r / 255 * 5)) +
+ (6 * (int) round($g / 255 * 5)) +
+ (int) round($b / 255 * 5);
+ }
+}
diff --git a/Output/BufferedOutput.php b/Output/BufferedOutput.php
index d37c6e323..3c8d3906f 100644
--- a/Output/BufferedOutput.php
+++ b/Output/BufferedOutput.php
@@ -16,14 +16,12 @@
*/
class BufferedOutput extends Output
{
- private $buffer = '';
+ private string $buffer = '';
/**
* Empties buffer and returns its content.
- *
- * @return string
*/
- public function fetch()
+ public function fetch(): string
{
$content = $this->buffer;
$this->buffer = '';
@@ -31,10 +29,7 @@ public function fetch()
return $content;
}
- /**
- * {@inheritdoc}
- */
- protected function doWrite(string $message, bool $newline)
+ protected function doWrite(string $message, bool $newline): void
{
$this->buffer .= $message;
diff --git a/Output/ConsoleOutput.php b/Output/ConsoleOutput.php
index 560aeb581..2ad3dbcf3 100644
--- a/Output/ConsoleOutput.php
+++ b/Output/ConsoleOutput.php
@@ -29,8 +29,8 @@
*/
class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
{
- private $stderr;
- private $consoleSectionOutputs = [];
+ private OutputInterface $stderr;
+ private array $consoleSectionOutputs = [];
/**
* @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface)
@@ -64,45 +64,30 @@ public function section(): ConsoleSectionOutput
return new ConsoleSectionOutput($this->getStream(), $this->consoleSectionOutputs, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter());
}
- /**
- * {@inheritdoc}
- */
- public function setDecorated(bool $decorated)
+ public function setDecorated(bool $decorated): void
{
parent::setDecorated($decorated);
$this->stderr->setDecorated($decorated);
}
- /**
- * {@inheritdoc}
- */
- public function setFormatter(OutputFormatterInterface $formatter)
+ public function setFormatter(OutputFormatterInterface $formatter): void
{
parent::setFormatter($formatter);
$this->stderr->setFormatter($formatter);
}
- /**
- * {@inheritdoc}
- */
- public function setVerbosity(int $level)
+ public function setVerbosity(int $level): void
{
parent::setVerbosity($level);
$this->stderr->setVerbosity($level);
}
- /**
- * {@inheritdoc}
- */
- public function getErrorOutput()
+ public function getErrorOutput(): OutputInterface
{
return $this->stderr;
}
- /**
- * {@inheritdoc}
- */
- public function setErrorOutput(OutputInterface $error)
+ public function setErrorOutput(OutputInterface $error): void
{
$this->stderr = $error;
}
@@ -110,10 +95,8 @@ public function setErrorOutput(OutputInterface $error)
/**
* Returns true if current environment supports writing console output to
* STDOUT.
- *
- * @return bool
*/
- protected function hasStdoutSupport()
+ protected function hasStdoutSupport(): bool
{
return false === $this->isRunningOS400();
}
@@ -121,10 +104,8 @@ protected function hasStdoutSupport()
/**
* Returns true if current environment supports writing console output to
* STDERR.
- *
- * @return bool
*/
- protected function hasStderrSupport()
+ protected function hasStderrSupport(): bool
{
return false === $this->isRunningOS400();
}
diff --git a/Output/ConsoleOutputInterface.php b/Output/ConsoleOutputInterface.php
index 6b6635f58..1f8f147ce 100644
--- a/Output/ConsoleOutputInterface.php
+++ b/Output/ConsoleOutputInterface.php
@@ -21,12 +21,10 @@ interface ConsoleOutputInterface extends OutputInterface
{
/**
* Gets the OutputInterface for errors.
- *
- * @return OutputInterface
*/
- public function getErrorOutput();
+ public function getErrorOutput(): OutputInterface;
- public function setErrorOutput(OutputInterface $error);
+ public function setErrorOutput(OutputInterface $error): void;
public function section(): ConsoleSectionOutput;
}
diff --git a/Output/ConsoleSectionOutput.php b/Output/ConsoleSectionOutput.php
index 70d70c50b..44728dfd4 100644
--- a/Output/ConsoleSectionOutput.php
+++ b/Output/ConsoleSectionOutput.php
@@ -21,10 +21,11 @@
*/
class ConsoleSectionOutput extends StreamOutput
{
- private $content = [];
- private $lines = 0;
- private $sections;
- private $terminal;
+ private array $content = [];
+ private int $lines = 0;
+ private array $sections;
+ private Terminal $terminal;
+ private int $maxHeight = 0;
/**
* @param resource $stream
@@ -38,19 +39,36 @@ public function __construct($stream, array &$sections, int $verbosity, bool $dec
$this->terminal = new Terminal();
}
+ /**
+ * Defines a maximum number of lines for this section.
+ *
+ * When more lines are added, the section will automatically scroll to the
+ * end (i.e. remove the first lines to comply with the max height).
+ */
+ public function setMaxHeight(int $maxHeight): void
+ {
+ // when changing max height, clear output of current section and redraw again with the new height
+ $previousMaxHeight = $this->maxHeight;
+ $this->maxHeight = $maxHeight;
+ $existingContent = $this->popStreamContentUntilCurrentSection($previousMaxHeight ? min($previousMaxHeight, $this->lines) : $this->lines);
+
+ parent::doWrite($this->getVisibleContent(), false);
+ parent::doWrite($existingContent, false);
+ }
+
/**
* Clears previous output for this section.
*
* @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared
*/
- public function clear(?int $lines = null)
+ public function clear(?int $lines = null): void
{
- if (empty($this->content) || !$this->isDecorated()) {
+ if (!$this->content || !$this->isDecorated()) {
return;
}
if ($lines) {
- array_splice($this->content, -($lines * 2)); // Multiply lines by 2 to cater for each new line added between content
+ array_splice($this->content, -$lines);
} else {
$lines = $this->lines;
$this->content = [];
@@ -58,15 +76,13 @@ public function clear(?int $lines = null)
$this->lines -= $lines;
- parent::doWrite($this->popStreamContentUntilCurrentSection($lines), false);
+ parent::doWrite($this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $lines) : $lines), false);
}
/**
* Overwrites the previous output with a new message.
- *
- * @param array|string $message
*/
- public function overwrite($message)
+ public function overwrite(string|iterable $message): void
{
$this->clear();
$this->writeln($message);
@@ -77,34 +93,107 @@ public function getContent(): string
return implode('', $this->content);
}
+ public function getVisibleContent(): string
+ {
+ if (0 === $this->maxHeight) {
+ return $this->getContent();
+ }
+
+ return implode('', \array_slice($this->content, -$this->maxHeight));
+ }
+
/**
* @internal
*/
- public function addContent(string $input)
+ public function addContent(string $input, bool $newline = true): int
{
- foreach (explode(\PHP_EOL, $input) as $lineContent) {
- $this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1;
- $this->content[] = $lineContent;
- $this->content[] = \PHP_EOL;
+ $width = $this->terminal->getWidth();
+ $lines = explode(\PHP_EOL, $input);
+ $linesAdded = 0;
+ $count = \count($lines) - 1;
+ foreach ($lines as $i => $lineContent) {
+ // re-add the line break (that has been removed in the above `explode()` for
+ // - every line that is not the last line
+ // - if $newline is required, also add it to the last line
+ if ($i < $count || $newline) {
+ $lineContent .= \PHP_EOL;
+ }
+
+ // skip line if there is no text (or newline for that matter)
+ if ('' === $lineContent) {
+ continue;
+ }
+
+ // For the first line, check if the previous line (last entry of `$this->content`)
+ // needs to be continued (i.e. does not end with a line break).
+ if (0 === $i
+ && (false !== $lastLine = end($this->content))
+ && !str_ends_with($lastLine, \PHP_EOL)
+ ) {
+ // deduct the line count of the previous line
+ $this->lines -= (int) ceil($this->getDisplayLength($lastLine) / $width) ?: 1;
+ // concatenate previous and new line
+ $lineContent = $lastLine.$lineContent;
+ // replace last entry of `$this->content` with the new expanded line
+ array_splice($this->content, -1, 1, $lineContent);
+ } else {
+ // otherwise just add the new content
+ $this->content[] = $lineContent;
+ }
+
+ $linesAdded += (int) ceil($this->getDisplayLength($lineContent) / $width) ?: 1;
}
+
+ $this->lines += $linesAdded;
+
+ return $linesAdded;
}
/**
- * {@inheritdoc}
+ * @internal
*/
- protected function doWrite(string $message, bool $newline)
+ public function addNewLineOfInputSubmit(): void
+ {
+ $this->content[] = \PHP_EOL;
+ ++$this->lines;
+ }
+
+ protected function doWrite(string $message, bool $newline): void
{
+ // Simulate newline behavior for consistent output formatting, avoiding extra logic
+ if (!$newline && str_ends_with($message, \PHP_EOL)) {
+ $message = substr($message, 0, -\strlen(\PHP_EOL));
+ $newline = true;
+ }
+
if (!$this->isDecorated()) {
parent::doWrite($message, $newline);
return;
}
- $erasedContent = $this->popStreamContentUntilCurrentSection();
+ // Check if the previous line (last entry of `$this->content`) needs to be continued
+ // (i.e. does not end with a line break). In which case, it needs to be erased first.
+ $linesToClear = $deleteLastLine = ($lastLine = end($this->content) ?: '') && !str_ends_with($lastLine, \PHP_EOL) ? 1 : 0;
+
+ $linesAdded = $this->addContent($message, $newline);
+
+ if ($lineOverflow = $this->maxHeight > 0 && $this->lines > $this->maxHeight) {
+ // on overflow, clear the whole section and redraw again (to remove the first lines)
+ $linesToClear = $this->maxHeight;
+ }
+
+ $erasedContent = $this->popStreamContentUntilCurrentSection($linesToClear);
- $this->addContent($message);
+ if ($lineOverflow) {
+ // redraw existing lines of the section
+ $previousLinesOfSection = \array_slice($this->content, $this->lines - $this->maxHeight, $this->maxHeight - $linesAdded);
+ parent::doWrite(implode('', $previousLinesOfSection), false);
+ }
- parent::doWrite($message, true);
+ // if the last line was removed, re-print its content together with the new content.
+ // otherwise, just print the new content.
+ parent::doWrite($deleteLastLine ? $lastLine.$message : $message, true);
parent::doWrite($erasedContent, false);
}
@@ -122,13 +211,18 @@ private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFr
break;
}
- $numberOfLinesToClear += $section->lines;
- $erasedContent[] = $section->getContent();
+ $numberOfLinesToClear += $section->maxHeight ? min($section->lines, $section->maxHeight) : $section->lines;
+ if ('' !== $sectionContent = $section->getVisibleContent()) {
+ if (!str_ends_with($sectionContent, \PHP_EOL)) {
+ $sectionContent .= \PHP_EOL;
+ }
+ $erasedContent[] = $sectionContent;
+ }
}
if ($numberOfLinesToClear > 0) {
// move cursor up n lines
- parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false);
+ parent::doWrite(\sprintf("\x1b[%dA", $numberOfLinesToClear), false);
// erase to end of screen
parent::doWrite("\x1b[0J", false);
}
diff --git a/Output/NullOutput.php b/Output/NullOutput.php
index 3bbe63ea0..8bec706d4 100644
--- a/Output/NullOutput.php
+++ b/Output/NullOutput.php
@@ -24,104 +24,70 @@
*/
class NullOutput implements OutputInterface
{
- private $formatter;
+ private NullOutputFormatter $formatter;
- /**
- * {@inheritdoc}
- */
- public function setFormatter(OutputFormatterInterface $formatter)
+ public function setFormatter(OutputFormatterInterface $formatter): void
{
// do nothing
}
- /**
- * {@inheritdoc}
- */
- public function getFormatter()
+ public function getFormatter(): OutputFormatterInterface
{
- if ($this->formatter) {
- return $this->formatter;
- }
// to comply with the interface we must return a OutputFormatterInterface
- return $this->formatter = new NullOutputFormatter();
+ return $this->formatter ??= new NullOutputFormatter();
}
- /**
- * {@inheritdoc}
- */
- public function setDecorated(bool $decorated)
+ public function setDecorated(bool $decorated): void
{
// do nothing
}
- /**
- * {@inheritdoc}
- */
- public function isDecorated()
+ public function isDecorated(): bool
{
return false;
}
- /**
- * {@inheritdoc}
- */
- public function setVerbosity(int $level)
+ public function setVerbosity(int $level): void
{
// do nothing
}
- /**
- * {@inheritdoc}
- */
- public function getVerbosity()
+ public function getVerbosity(): int
{
- return self::VERBOSITY_QUIET;
+ return self::VERBOSITY_SILENT;
}
- /**
- * {@inheritdoc}
- */
- public function isQuiet()
+ public function isSilent(): bool
{
return true;
}
- /**
- * {@inheritdoc}
- */
- public function isVerbose()
+ public function isQuiet(): bool
{
return false;
}
- /**
- * {@inheritdoc}
- */
- public function isVeryVerbose()
+ public function isVerbose(): bool
{
return false;
}
- /**
- * {@inheritdoc}
- */
- public function isDebug()
+ public function isVeryVerbose(): bool
{
return false;
}
- /**
- * {@inheritdoc}
- */
- public function writeln($messages, int $options = self::OUTPUT_NORMAL)
+ public function isDebug(): bool
+ {
+ return false;
+ }
+
+ public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL): void
{
// do nothing
}
- /**
- * {@inheritdoc}
- */
- public function write($messages, bool $newline = false, int $options = self::OUTPUT_NORMAL)
+ public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL): void
{
// do nothing
}
diff --git a/Output/Output.php b/Output/Output.php
index 28c40bb3e..32e6cb241 100644
--- a/Output/Output.php
+++ b/Output/Output.php
@@ -17,20 +17,21 @@
/**
* Base class for output classes.
*
- * There are five levels of verbosity:
+ * There are six levels of verbosity:
*
* * normal: no option passed (normal output)
* * verbose: -v (more output)
* * very verbose: -vv (highly extended output)
* * debug: -vvv (all debug output)
- * * quiet: -q (no output)
+ * * quiet: -q (only output errors)
+ * * silent: --silent (no output)
*
* @author Fabien Potencier
*/
abstract class Output implements OutputInterface
{
- private $verbosity;
- private $formatter;
+ private int $verbosity;
+ private OutputFormatterInterface $formatter;
/**
* @param int|null $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface)
@@ -44,98 +45,67 @@ public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $deco
$this->formatter->setDecorated($decorated);
}
- /**
- * {@inheritdoc}
- */
- public function setFormatter(OutputFormatterInterface $formatter)
+ public function setFormatter(OutputFormatterInterface $formatter): void
{
$this->formatter = $formatter;
}
- /**
- * {@inheritdoc}
- */
- public function getFormatter()
+ public function getFormatter(): OutputFormatterInterface
{
return $this->formatter;
}
- /**
- * {@inheritdoc}
- */
- public function setDecorated(bool $decorated)
+ public function setDecorated(bool $decorated): void
{
$this->formatter->setDecorated($decorated);
}
- /**
- * {@inheritdoc}
- */
- public function isDecorated()
+ public function isDecorated(): bool
{
return $this->formatter->isDecorated();
}
- /**
- * {@inheritdoc}
- */
- public function setVerbosity(int $level)
+ public function setVerbosity(int $level): void
{
$this->verbosity = $level;
}
- /**
- * {@inheritdoc}
- */
- public function getVerbosity()
+ public function getVerbosity(): int
{
return $this->verbosity;
}
- /**
- * {@inheritdoc}
- */
- public function isQuiet()
+ public function isSilent(): bool
+ {
+ return self::VERBOSITY_SILENT === $this->verbosity;
+ }
+
+ public function isQuiet(): bool
{
return self::VERBOSITY_QUIET === $this->verbosity;
}
- /**
- * {@inheritdoc}
- */
- public function isVerbose()
+ public function isVerbose(): bool
{
return self::VERBOSITY_VERBOSE <= $this->verbosity;
}
- /**
- * {@inheritdoc}
- */
- public function isVeryVerbose()
+ public function isVeryVerbose(): bool
{
return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity;
}
- /**
- * {@inheritdoc}
- */
- public function isDebug()
+ public function isDebug(): bool
{
return self::VERBOSITY_DEBUG <= $this->verbosity;
}
- /**
- * {@inheritdoc}
- */
- public function writeln($messages, int $options = self::OUTPUT_NORMAL)
+ public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL): void
{
$this->write($messages, true, $options);
}
- /**
- * {@inheritdoc}
- */
- public function write($messages, bool $newline = false, int $options = self::OUTPUT_NORMAL)
+ public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL): void
{
if (!is_iterable($messages)) {
$messages = [$messages];
@@ -170,5 +140,5 @@ public function write($messages, bool $newline = false, int $options = self::OUT
/**
* Writes a message to the output.
*/
- abstract protected function doWrite(string $message, bool $newline);
+ abstract protected function doWrite(string $message, bool $newline): void;
}
diff --git a/Output/OutputInterface.php b/Output/OutputInterface.php
index 55caab80b..969a3b022 100644
--- a/Output/OutputInterface.php
+++ b/Output/OutputInterface.php
@@ -17,9 +17,12 @@
* OutputInterface is the interface implemented by all Output classes.
*
* @author Fabien Potencier
+ *
+ * @method bool isSilent()
*/
interface OutputInterface
{
+ public const VERBOSITY_SILENT = 8;
public const VERBOSITY_QUIET = 16;
public const VERBOSITY_NORMAL = 32;
public const VERBOSITY_VERBOSE = 64;
@@ -33,78 +36,68 @@ interface OutputInterface
/**
* Writes a message to the output.
*
- * @param string|iterable $messages The message as an iterable of strings or a single string
- * @param bool $newline Whether to add a newline
- * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL
+ * @param bool $newline Whether to add a newline
+ * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants),
+ * 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL
*/
- public function write($messages, bool $newline = false, int $options = 0);
+ public function write(string|iterable $messages, bool $newline = false, int $options = 0): void;
/**
* Writes a message to the output and adds a newline at the end.
*
- * @param string|iterable $messages The message as an iterable of strings or a single string
- * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL
+ * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants),
+ * 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL
*/
- public function writeln($messages, int $options = 0);
+ public function writeln(string|iterable $messages, int $options = 0): void;
/**
* Sets the verbosity of the output.
+ *
+ * @param self::VERBOSITY_* $level
*/
- public function setVerbosity(int $level);
+ public function setVerbosity(int $level): void;
/**
* Gets the current verbosity of the output.
*
- * @return int
+ * @return self::VERBOSITY_*
*/
- public function getVerbosity();
+ public function getVerbosity(): int;
/**
* Returns whether verbosity is quiet (-q).
- *
- * @return bool
*/
- public function isQuiet();
+ public function isQuiet(): bool;
/**
* Returns whether verbosity is verbose (-v).
- *
- * @return bool
*/
- public function isVerbose();
+ public function isVerbose(): bool;
/**
* Returns whether verbosity is very verbose (-vv).
- *
- * @return bool
*/
- public function isVeryVerbose();
+ public function isVeryVerbose(): bool;
/**
* Returns whether verbosity is debug (-vvv).
- *
- * @return bool
*/
- public function isDebug();
+ public function isDebug(): bool;
/**
* Sets the decorated flag.
*/
- public function setDecorated(bool $decorated);
+ public function setDecorated(bool $decorated): void;
/**
* Gets the decorated flag.
- *
- * @return bool
*/
- public function isDecorated();
+ public function isDecorated(): bool;
- public function setFormatter(OutputFormatterInterface $formatter);
+ public function setFormatter(OutputFormatterInterface $formatter): void;
/**
* Returns current output formatter instance.
- *
- * @return OutputFormatterInterface
*/
- public function getFormatter();
+ public function getFormatter(): OutputFormatterInterface;
}
diff --git a/Output/StreamOutput.php b/Output/StreamOutput.php
index b53955269..ce5a825e8 100644
--- a/Output/StreamOutput.php
+++ b/Output/StreamOutput.php
@@ -29,6 +29,7 @@
*/
class StreamOutput extends Output
{
+ /** @var resource */
private $stream;
/**
@@ -47,9 +48,7 @@ public function __construct($stream, int $verbosity = self::VERBOSITY_NORMAL, ?b
$this->stream = $stream;
- if (null === $decorated) {
- $decorated = $this->hasColorSupport();
- }
+ $decorated ??= $this->hasColorSupport();
parent::__construct($verbosity, $decorated, $formatter);
}
@@ -64,7 +63,7 @@ public function getStream()
return $this->stream;
}
- protected function doWrite(string $message, bool $newline)
+ protected function doWrite(string $message, bool $newline): void
{
if ($newline) {
$message .= \PHP_EOL;
@@ -88,13 +87,18 @@ protected function doWrite(string $message, bool $newline)
*
* @return bool true if the stream supports colorization, false otherwise
*/
- protected function hasColorSupport()
+ protected function hasColorSupport(): bool
{
// Follow https://no-color.org/
if ('' !== (($_SERVER['NO_COLOR'] ?? getenv('NO_COLOR'))[0] ?? '')) {
return false;
}
+ // Follow https://force-color.org/
+ if ('' !== (($_SERVER['FORCE_COLOR'] ?? getenv('FORCE_COLOR'))[0] ?? '')) {
+ return true;
+ }
+
// Detect msysgit/mingw and assume this is a tty because detection
// does not work correctly, see https://github.com/composer/composer/issues/9690
if (!@stream_isatty($this->stream) && !\in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) {
diff --git a/Output/TrimmedBufferOutput.php b/Output/TrimmedBufferOutput.php
index b08503b3a..33db072c5 100644
--- a/Output/TrimmedBufferOutput.php
+++ b/Output/TrimmedBufferOutput.php
@@ -21,13 +21,13 @@
*/
class TrimmedBufferOutput extends Output
{
- private $maxLength;
- private $buffer = '';
+ private int $maxLength;
+ private string $buffer = '';
public function __construct(int $maxLength, ?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, ?OutputFormatterInterface $formatter = null)
{
if ($maxLength <= 0) {
- throw new InvalidArgumentException(sprintf('"%s()" expects a strictly positive maxLength. Got %d.', __METHOD__, $maxLength));
+ throw new InvalidArgumentException(\sprintf('"%s()" expects a strictly positive maxLength. Got %d.', __METHOD__, $maxLength));
}
parent::__construct($verbosity, $decorated, $formatter);
@@ -36,10 +36,8 @@ public function __construct(int $maxLength, ?int $verbosity = self::VERBOSITY_NO
/**
* Empties buffer and returns its content.
- *
- * @return string
*/
- public function fetch()
+ public function fetch(): string
{
$content = $this->buffer;
$this->buffer = '';
@@ -47,10 +45,7 @@ public function fetch()
return $content;
}
- /**
- * {@inheritdoc}
- */
- protected function doWrite(string $message, bool $newline)
+ protected function doWrite(string $message, bool $newline): void
{
$this->buffer .= $message;
@@ -58,6 +53,6 @@ protected function doWrite(string $message, bool $newline)
$this->buffer .= \PHP_EOL;
}
- $this->buffer = substr($this->buffer, 0 - $this->maxLength);
+ $this->buffer = substr($this->buffer, -$this->maxLength);
}
}
diff --git a/Question/ChoiceQuestion.php b/Question/ChoiceQuestion.php
index bf1f90487..36c240d37 100644
--- a/Question/ChoiceQuestion.php
+++ b/Question/ChoiceQuestion.php
@@ -20,35 +20,34 @@
*/
class ChoiceQuestion extends Question
{
- private $choices;
- private $multiselect = false;
- private $prompt = ' > ';
- private $errorMessage = 'Value "%s" is invalid';
+ private bool $multiselect = false;
+ private string $prompt = ' > ';
+ private string $errorMessage = 'Value "%s" is invalid';
/**
- * @param string $question The question to ask to the user
- * @param array $choices The list of available choices
- * @param mixed $default The default answer to return
+ * @param string $question The question to ask to the user
+ * @param array $choices The list of available choices
+ * @param string|bool|int|float|null $default The default answer to return
*/
- public function __construct(string $question, array $choices, $default = null)
- {
+ public function __construct(
+ string $question,
+ private array $choices,
+ string|bool|int|float|null $default = null,
+ ) {
if (!$choices) {
throw new \LogicException('Choice question must have at least 1 choice available.');
}
parent::__construct($question, $default);
- $this->choices = $choices;
$this->setValidator($this->getDefaultValidator());
$this->setAutocompleterValues($choices);
}
/**
* Returns available choices.
- *
- * @return array
*/
- public function getChoices()
+ public function getChoices(): array
{
return $this->choices;
}
@@ -60,7 +59,7 @@ public function getChoices()
*
* @return $this
*/
- public function setMultiselect(bool $multiselect)
+ public function setMultiselect(bool $multiselect): static
{
$this->multiselect = $multiselect;
$this->setValidator($this->getDefaultValidator());
@@ -70,20 +69,16 @@ public function setMultiselect(bool $multiselect)
/**
* Returns whether the choices are multiselect.
- *
- * @return bool
*/
- public function isMultiselect()
+ public function isMultiselect(): bool
{
return $this->multiselect;
}
/**
* Gets the prompt for choices.
- *
- * @return string
*/
- public function getPrompt()
+ public function getPrompt(): string
{
return $this->prompt;
}
@@ -93,7 +88,7 @@ public function getPrompt()
*
* @return $this
*/
- public function setPrompt(string $prompt)
+ public function setPrompt(string $prompt): static
{
$this->prompt = $prompt;
@@ -107,7 +102,7 @@ public function setPrompt(string $prompt)
*
* @return $this
*/
- public function setErrorMessage(string $errorMessage)
+ public function setErrorMessage(string $errorMessage): static
{
$this->errorMessage = $errorMessage;
$this->setValidator($this->getDefaultValidator());
@@ -126,7 +121,7 @@ private function getDefaultValidator(): callable
if ($multiselect) {
// Check for a separated comma values
if (!preg_match('/^[^,]+(?:,[^,]+)*$/', (string) $selected, $matches)) {
- throw new InvalidArgumentException(sprintf($errorMessage, $selected));
+ throw new InvalidArgumentException(\sprintf($errorMessage, $selected));
}
$selectedChoices = explode(',', (string) $selected);
@@ -150,7 +145,7 @@ private function getDefaultValidator(): callable
}
if (\count($results) > 1) {
- throw new InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of "%s".', implode('" or "', $results)));
+ throw new InvalidArgumentException(\sprintf('The provided answer is ambiguous. Value should be one of "%s".', implode('" or "', $results)));
}
$result = array_search($value, $choices);
@@ -166,7 +161,7 @@ private function getDefaultValidator(): callable
}
if (false === $result) {
- throw new InvalidArgumentException(sprintf($errorMessage, $value));
+ throw new InvalidArgumentException(\sprintf($errorMessage, $value));
}
// For associative choices, consistently return the key as string:
diff --git a/Question/ConfirmationQuestion.php b/Question/ConfirmationQuestion.php
index 4228521b9..951d68140 100644
--- a/Question/ConfirmationQuestion.php
+++ b/Question/ConfirmationQuestion.php
@@ -18,18 +18,18 @@
*/
class ConfirmationQuestion extends Question
{
- private $trueAnswerRegex;
-
/**
* @param string $question The question to ask to the user
* @param bool $default The default answer to return, true or false
* @param string $trueAnswerRegex A regex to match the "yes" answer
*/
- public function __construct(string $question, bool $default = true, string $trueAnswerRegex = '/^y/i')
- {
+ public function __construct(
+ string $question,
+ bool $default = true,
+ private string $trueAnswerRegex = '/^y/i',
+ ) {
parent::__construct($question, $default);
- $this->trueAnswerRegex = $trueAnswerRegex;
$this->setNormalizer($this->getDefaultNormalizer());
}
diff --git a/Question/Question.php b/Question/Question.php
index ba5744283..46a60c798 100644
--- a/Question/Question.php
+++ b/Question/Question.php
@@ -21,43 +21,37 @@
*/
class Question
{
- private $question;
- private $attempts;
- private $hidden = false;
- private $hiddenFallback = true;
- private $autocompleterCallback;
- private $validator;
- private $default;
- private $normalizer;
- private $trimmable = true;
- private $multiline = false;
+ private ?int $attempts = null;
+ private bool $hidden = false;
+ private bool $hiddenFallback = true;
+ private ?\Closure $autocompleterCallback = null;
+ private ?\Closure $validator = null;
+ private ?\Closure $normalizer = null;
+ private bool $trimmable = true;
+ private bool $multiline = false;
/**
* @param string $question The question to ask to the user
* @param string|bool|int|float|null $default The default answer to return if the user enters nothing
*/
- public function __construct(string $question, $default = null)
- {
- $this->question = $question;
- $this->default = $default;
+ public function __construct(
+ private string $question,
+ private string|bool|int|float|null $default = null,
+ ) {
}
/**
* Returns the question.
- *
- * @return string
*/
- public function getQuestion()
+ public function getQuestion(): string
{
return $this->question;
}
/**
* Returns the default answer.
- *
- * @return string|bool|int|float|null
*/
- public function getDefault()
+ public function getDefault(): string|bool|int|float|null
{
return $this->default;
}
@@ -75,7 +69,7 @@ public function isMultiline(): bool
*
* @return $this
*/
- public function setMultiline(bool $multiline): self
+ public function setMultiline(bool $multiline): static
{
$this->multiline = $multiline;
@@ -84,10 +78,8 @@ public function setMultiline(bool $multiline): self
/**
* Returns whether the user response must be hidden.
- *
- * @return bool
*/
- public function isHidden()
+ public function isHidden(): bool
{
return $this->hidden;
}
@@ -99,7 +91,7 @@ public function isHidden()
*
* @throws LogicException In case the autocompleter is also used
*/
- public function setHidden(bool $hidden)
+ public function setHidden(bool $hidden): static
{
if ($this->autocompleterCallback) {
throw new LogicException('A hidden question cannot use the autocompleter.');
@@ -112,10 +104,8 @@ public function setHidden(bool $hidden)
/**
* In case the response cannot be hidden, whether to fallback on non-hidden question or not.
- *
- * @return bool
*/
- public function isHiddenFallback()
+ public function isHiddenFallback(): bool
{
return $this->hiddenFallback;
}
@@ -125,7 +115,7 @@ public function isHiddenFallback()
*
* @return $this
*/
- public function setHiddenFallback(bool $fallback)
+ public function setHiddenFallback(bool $fallback): static
{
$this->hiddenFallback = $fallback;
@@ -134,10 +124,8 @@ public function setHiddenFallback(bool $fallback)
/**
* Gets values for the autocompleter.
- *
- * @return iterable|null
*/
- public function getAutocompleterValues()
+ public function getAutocompleterValues(): ?iterable
{
$callback = $this->getAutocompleterCallback();
@@ -151,18 +139,17 @@ public function getAutocompleterValues()
*
* @throws LogicException
*/
- public function setAutocompleterValues(?iterable $values)
+ public function setAutocompleterValues(?iterable $values): static
{
if (\is_array($values)) {
$values = $this->isAssoc($values) ? array_merge(array_keys($values), array_values($values)) : array_values($values);
- $callback = static function () use ($values) {
- return $values;
- };
+ $callback = static fn () => $values;
} elseif ($values instanceof \Traversable) {
- $valueCache = null;
- $callback = static function () use ($values, &$valueCache) {
- return $valueCache ?? $valueCache = iterator_to_array($values, false);
+ $callback = static function () use ($values) {
+ static $valueCache;
+
+ return $valueCache ??= iterator_to_array($values, false);
};
} else {
$callback = null;
@@ -186,13 +173,13 @@ public function getAutocompleterCallback(): ?callable
*
* @return $this
*/
- public function setAutocompleterCallback(?callable $callback = null): self
+ public function setAutocompleterCallback(?callable $callback): static
{
if ($this->hidden && null !== $callback) {
throw new LogicException('A hidden question cannot use the autocompleter.');
}
- $this->autocompleterCallback = $callback;
+ $this->autocompleterCallback = null === $callback ? null : $callback(...);
return $this;
}
@@ -202,19 +189,17 @@ public function setAutocompleterCallback(?callable $callback = null): self
*
* @return $this
*/
- public function setValidator(?callable $validator = null)
+ public function setValidator(?callable $validator): static
{
- $this->validator = $validator;
+ $this->validator = null === $validator ? null : $validator(...);
return $this;
}
/**
* Gets the validator for the question.
- *
- * @return callable|null
*/
- public function getValidator()
+ public function getValidator(): ?callable
{
return $this->validator;
}
@@ -228,7 +213,7 @@ public function getValidator()
*
* @throws InvalidArgumentException in case the number of attempts is invalid
*/
- public function setMaxAttempts(?int $attempts)
+ public function setMaxAttempts(?int $attempts): static
{
if (null !== $attempts && $attempts < 1) {
throw new InvalidArgumentException('Maximum number of attempts must be a positive value.');
@@ -243,10 +228,8 @@ public function setMaxAttempts(?int $attempts)
* Gets the maximum number of attempts.
*
* Null means an unlimited number of attempts.
- *
- * @return int|null
*/
- public function getMaxAttempts()
+ public function getMaxAttempts(): ?int
{
return $this->attempts;
}
@@ -258,9 +241,9 @@ public function getMaxAttempts()
*
* @return $this
*/
- public function setNormalizer(callable $normalizer)
+ public function setNormalizer(callable $normalizer): static
{
- $this->normalizer = $normalizer;
+ $this->normalizer = $normalizer(...);
return $this;
}
@@ -269,15 +252,13 @@ public function setNormalizer(callable $normalizer)
* Gets the normalizer for the response.
*
* The normalizer can ba a callable (a string), a closure or a class implementing __invoke.
- *
- * @return callable|null
*/
- public function getNormalizer()
+ public function getNormalizer(): ?callable
{
return $this->normalizer;
}
- protected function isAssoc(array $array)
+ protected function isAssoc(array $array): bool
{
return (bool) \count(array_filter(array_keys($array), 'is_string'));
}
@@ -290,7 +271,7 @@ public function isTrimmable(): bool
/**
* @return $this
*/
- public function setTrimmable(bool $trimmable): self
+ public function setTrimmable(bool $trimmable): static
{
$this->trimmable = $trimmable;
diff --git a/README.md b/README.md
index c4c129989..92f70e714 100644
--- a/README.md
+++ b/README.md
@@ -7,14 +7,7 @@ interfaces.
Sponsor
-------
-The Console component for Symfony 5.4/6.0 is [backed][1] by [Les-Tilleuls.coop][2].
-
-Les-Tilleuls.coop is a team of 50+ Symfony experts who can help you design, develop and
-fix your projects. We provide a wide range of professional services including development,
-consulting, coaching, training and audits. We also are highly skilled in JS, Go and DevOps.
-We are a worker cooperative!
-
-Help Symfony by [sponsoring][3] its development!
+Help Symfony by [sponsoring][1] its development!
Resources
---------
@@ -31,6 +24,4 @@ Credits
`Resources/bin/hiddeninput.exe` is a third party binary provided within this
component. Find sources and license at https://github.com/Seldaek/hidden-input.
-[1]: https://symfony.com/backers
-[2]: https://les-tilleuls.coop
-[3]: https://symfony.com/sponsor
+[1]: https://symfony.com/sponsor
diff --git a/Resources/completion.bash b/Resources/completion.bash
index bb44037b0..64c6a338f 100644
--- a/Resources/completion.bash
+++ b/Resources/completion.bash
@@ -6,6 +6,16 @@
# https://symfony.com/doc/current/contributing/code/license.html
_sf_{{ COMMAND_NAME }}() {
+
+ # Use the default completion for shell redirect operators.
+ for w in '>' '>>' '&>' '<'; do
+ if [[ $w = "${COMP_WORDS[COMP_CWORD-1]}" ]]; then
+ compopt -o filenames
+ COMPREPLY=($(compgen -f -- "${COMP_WORDS[COMP_CWORD]}"))
+ return 0
+ fi
+ done
+
# Use newline as only separator to allow space in completion values
local IFS=$'\n'
local sf_cmd="${COMP_WORDS[0]}"
@@ -25,7 +35,7 @@ _sf_{{ COMMAND_NAME }}() {
local cur prev words cword
_get_comp_words_by_ref -n := cur prev words cword
- local completecmd=("$sf_cmd" "_complete" "--no-interaction" "-sbash" "-c$cword" "-S{{ VERSION }}")
+ local completecmd=("$sf_cmd" "_complete" "--no-interaction" "-sbash" "-c$cword" "-a{{ VERSION }}")
for w in ${words[@]}; do
w=$(printf -- '%b' "$w")
# remove quotes from typed values
diff --git a/Resources/completion.fish b/Resources/completion.fish
new file mode 100644
index 000000000..1853dd80f
--- /dev/null
+++ b/Resources/completion.fish
@@ -0,0 +1,25 @@
+# This file is part of the Symfony package.
+#
+# (c) Fabien Potencier
+#
+# For the full copyright and license information, please view
+# https://symfony.com/doc/current/contributing/code/license.html
+
+function _sf_{{ COMMAND_NAME }}
+ set sf_cmd (commandline -o)
+ set c (count (commandline -oc))
+
+ set completecmd "$sf_cmd[1]" "_complete" "--no-interaction" "-sfish" "-a{{ VERSION }}"
+
+ for i in $sf_cmd
+ if [ $i != "" ]
+ set completecmd $completecmd "-i$i"
+ end
+ end
+
+ set completecmd $completecmd "-c$c"
+
+ $completecmd
+end
+
+complete -c '{{ COMMAND_NAME }}' -a '(_sf_{{ COMMAND_NAME }})' -f
diff --git a/Resources/completion.zsh b/Resources/completion.zsh
new file mode 100644
index 000000000..ff76fe5fa
--- /dev/null
+++ b/Resources/completion.zsh
@@ -0,0 +1,82 @@
+#compdef {{ COMMAND_NAME }}
+
+# This file is part of the Symfony package.
+#
+# (c) Fabien Potencier
+#
+# For the full copyright and license information, please view
+# https://symfony.com/doc/current/contributing/code/license.html
+
+#
+# zsh completions for {{ COMMAND_NAME }}
+#
+# References:
+# - https://github.com/spf13/cobra/blob/master/zsh_completions.go
+# - https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Console/Resources/completion.bash
+#
+_sf_{{ COMMAND_NAME }}() {
+ local lastParam flagPrefix requestComp out comp
+ local -a completions
+
+ # The user could have moved the cursor backwards on the command-line.
+ # We need to trigger completion from the $CURRENT location, so we need
+ # to truncate the command-line ($words) up to the $CURRENT location.
+ # (We cannot use $CURSOR as its value does not work when a command is an alias.)
+ words=("${=words[1,CURRENT]}") lastParam=${words[-1]}
+
+ # For zsh, when completing a flag with an = (e.g., {{ COMMAND_NAME }} -n=)
+ # completions must be prefixed with the flag
+ setopt local_options BASH_REMATCH
+ if [[ "${lastParam}" =~ '-.*=' ]]; then
+ # We are dealing with a flag with an =
+ flagPrefix="-P ${BASH_REMATCH}"
+ fi
+
+ # Prepare the command to obtain completions
+ requestComp="${words[0]} ${words[1]} _complete --no-interaction -szsh -a{{ VERSION }} -c$((CURRENT-1))" i=""
+ for w in ${words[@]}; do
+ w=$(printf -- '%b' "$w")
+ # remove quotes from typed values
+ quote="${w:0:1}"
+ if [ "$quote" = \' ]; then
+ w="${w%\'}"
+ w="${w#\'}"
+ elif [ "$quote" = \" ]; then
+ w="${w%\"}"
+ w="${w#\"}"
+ fi
+ # empty values are ignored
+ if [ ! -z "$w" ]; then
+ i="${i}-i${w} "
+ fi
+ done
+
+ # Ensure at least 1 input
+ if [ "${i}" = "" ]; then
+ requestComp="${requestComp} -i\" \""
+ else
+ requestComp="${requestComp} ${i}"
+ fi
+
+ # Use eval to handle any environment variables and such
+ out=$(eval ${requestComp} 2>/dev/null)
+
+ while IFS='\n' read -r comp; do
+ if [ -n "$comp" ]; then
+ # If requested, completions are returned with a description.
+ # The description is preceded by a TAB character.
+ # For zsh's _describe, we need to use a : instead of a TAB.
+ # We first need to escape any : as part of the completion itself.
+ comp=${comp//:/\\:}
+ local tab=$(printf '\t')
+ comp=${comp//$tab/:}
+ completions+=${comp}
+ fi
+ done < <(printf "%s\n" "${out[@]}")
+
+ # Let inbuilt _describe handle completions
+ eval _describe "completions" completions $flagPrefix
+ return $?
+}
+
+compdef _sf_{{ COMMAND_NAME }} {{ COMMAND_NAME }}
diff --git a/SignalRegistry/SignalMap.php b/SignalRegistry/SignalMap.php
new file mode 100644
index 000000000..2f9aa67c1
--- /dev/null
+++ b/SignalRegistry/SignalMap.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\SignalRegistry;
+
+/**
+ * @author Grégoire Pineau
+ */
+class SignalMap
+{
+ private static array $map;
+
+ public static function getSignalName(int $signal): ?string
+ {
+ if (!\extension_loaded('pcntl')) {
+ return null;
+ }
+
+ if (!isset(self::$map)) {
+ $r = new \ReflectionExtension('pcntl');
+ $c = $r->getConstants();
+ $map = array_filter($c, fn ($k) => str_starts_with($k, 'SIG') && !str_starts_with($k, 'SIG_') && 'SIGBABY' !== $k, \ARRAY_FILTER_USE_KEY);
+ self::$map = array_flip($map);
+ }
+
+ return self::$map[$signal] ?? null;
+ }
+}
diff --git a/SignalRegistry/SignalRegistry.php b/SignalRegistry/SignalRegistry.php
index 6bee24a42..8c2939eec 100644
--- a/SignalRegistry/SignalRegistry.php
+++ b/SignalRegistry/SignalRegistry.php
@@ -13,7 +13,7 @@
final class SignalRegistry
{
- private $signalHandlers = [];
+ private array $signalHandlers = [];
public function __construct()
{
@@ -34,20 +34,12 @@ public function register(int $signal, callable $signalHandler): void
$this->signalHandlers[$signal][] = $signalHandler;
- pcntl_signal($signal, [$this, 'handle']);
+ pcntl_signal($signal, $this->handle(...));
}
public static function isSupported(): bool
{
- if (!\function_exists('pcntl_signal')) {
- return false;
- }
-
- if (\in_array('pcntl_signal', explode(',', \ini_get('disable_functions')))) {
- return false;
- }
-
- return true;
+ return \function_exists('pcntl_signal');
}
/**
@@ -62,4 +54,12 @@ public function handle(int $signal): void
$signalHandler($signal, $hasNext);
}
}
+
+ /**
+ * @internal
+ */
+ public function scheduleAlarm(int $seconds): void
+ {
+ pcntl_alarm($seconds);
+ }
}
diff --git a/SingleCommandApplication.php b/SingleCommandApplication.php
index 774e5d8c4..2b54fb870 100644
--- a/SingleCommandApplication.php
+++ b/SingleCommandApplication.php
@@ -20,14 +20,14 @@
*/
class SingleCommandApplication extends Command
{
- private $version = 'UNKNOWN';
- private $autoExit = true;
- private $running = false;
+ private string $version = 'UNKNOWN';
+ private bool $autoExit = true;
+ private bool $running = false;
/**
* @return $this
*/
- public function setVersion(string $version): self
+ public function setVersion(string $version): static
{
$this->version = $version;
@@ -39,7 +39,7 @@ public function setVersion(string $version): self
*
* @return $this
*/
- public function setAutoExit(bool $autoExit): self
+ public function setAutoExit(bool $autoExit): static
{
$this->autoExit = $autoExit;
@@ -67,6 +67,6 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu
$this->running = false;
}
- return $ret ?? 1;
+ return $ret;
}
}
diff --git a/Style/OutputStyle.php b/Style/OutputStyle.php
index 67a98ff07..89a3a4177 100644
--- a/Style/OutputStyle.php
+++ b/Style/OutputStyle.php
@@ -23,126 +23,88 @@
*/
abstract class OutputStyle implements OutputInterface, StyleInterface
{
- private $output;
-
- public function __construct(OutputInterface $output)
- {
- $this->output = $output;
+ public function __construct(
+ private OutputInterface $output,
+ ) {
}
- /**
- * {@inheritdoc}
- */
- public function newLine(int $count = 1)
+ public function newLine(int $count = 1): void
{
$this->output->write(str_repeat(\PHP_EOL, $count));
}
- /**
- * @return ProgressBar
- */
- public function createProgressBar(int $max = 0)
+ public function createProgressBar(int $max = 0): ProgressBar
{
return new ProgressBar($this->output, $max);
}
- /**
- * {@inheritdoc}
- */
- public function write($messages, bool $newline = false, int $type = self::OUTPUT_NORMAL)
+ public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL): void
{
$this->output->write($messages, $newline, $type);
}
- /**
- * {@inheritdoc}
- */
- public function writeln($messages, int $type = self::OUTPUT_NORMAL)
+ public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL): void
{
$this->output->writeln($messages, $type);
}
- /**
- * {@inheritdoc}
- */
- public function setVerbosity(int $level)
+ public function setVerbosity(int $level): void
{
$this->output->setVerbosity($level);
}
- /**
- * {@inheritdoc}
- */
- public function getVerbosity()
+ public function getVerbosity(): int
{
return $this->output->getVerbosity();
}
- /**
- * {@inheritdoc}
- */
- public function setDecorated(bool $decorated)
+ public function setDecorated(bool $decorated): void
{
$this->output->setDecorated($decorated);
}
- /**
- * {@inheritdoc}
- */
- public function isDecorated()
+ public function isDecorated(): bool
{
return $this->output->isDecorated();
}
- /**
- * {@inheritdoc}
- */
- public function setFormatter(OutputFormatterInterface $formatter)
+ public function setFormatter(OutputFormatterInterface $formatter): void
{
$this->output->setFormatter($formatter);
}
- /**
- * {@inheritdoc}
- */
- public function getFormatter()
+ public function getFormatter(): OutputFormatterInterface
{
return $this->output->getFormatter();
}
- /**
- * {@inheritdoc}
- */
- public function isQuiet()
+ public function isSilent(): bool
+ {
+ // @deprecated since Symfony 7.2, change to $this->output->isSilent() in 8.0
+ return method_exists($this->output, 'isSilent') ? $this->output->isSilent() : self::VERBOSITY_SILENT === $this->output->getVerbosity();
+ }
+
+ public function isQuiet(): bool
{
return $this->output->isQuiet();
}
- /**
- * {@inheritdoc}
- */
- public function isVerbose()
+ public function isVerbose(): bool
{
return $this->output->isVerbose();
}
- /**
- * {@inheritdoc}
- */
- public function isVeryVerbose()
+ public function isVeryVerbose(): bool
{
return $this->output->isVeryVerbose();
}
- /**
- * {@inheritdoc}
- */
- public function isDebug()
+ public function isDebug(): bool
{
return $this->output->isDebug();
}
- protected function getErrorOutput()
+ protected function getErrorOutput(): OutputInterface
{
if (!$this->output instanceof ConsoleOutputInterface) {
return $this->output;
diff --git a/Style/StyleInterface.php b/Style/StyleInterface.php
index 9f25a43f6..fcc5bc775 100644
--- a/Style/StyleInterface.php
+++ b/Style/StyleInterface.php
@@ -21,112 +21,90 @@ interface StyleInterface
/**
* Formats a command title.
*/
- public function title(string $message);
+ public function title(string $message): void;
/**
* Formats a section title.
*/
- public function section(string $message);
+ public function section(string $message): void;
/**
* Formats a list.
*/
- public function listing(array $elements);
+ public function listing(array $elements): void;
/**
* Formats informational text.
- *
- * @param string|array $message
*/
- public function text($message);
+ public function text(string|array $message): void;
/**
* Formats a success result bar.
- *
- * @param string|array $message
*/
- public function success($message);
+ public function success(string|array $message): void;
/**
* Formats an error result bar.
- *
- * @param string|array $message
*/
- public function error($message);
+ public function error(string|array $message): void;
/**
* Formats an warning result bar.
- *
- * @param string|array $message
*/
- public function warning($message);
+ public function warning(string|array $message): void;
/**
* Formats a note admonition.
- *
- * @param string|array $message
*/
- public function note($message);
+ public function note(string|array $message): void;
/**
* Formats a caution admonition.
- *
- * @param string|array $message
*/
- public function caution($message);
+ public function caution(string|array $message): void;
/**
* Formats a table.
*/
- public function table(array $headers, array $rows);
+ public function table(array $headers, array $rows): void;
/**
* Asks a question.
- *
- * @return mixed
*/
- public function ask(string $question, ?string $default = null, ?callable $validator = null);
+ public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed;
/**
* Asks a question with the user input hidden.
- *
- * @return mixed
*/
- public function askHidden(string $question, ?callable $validator = null);
+ public function askHidden(string $question, ?callable $validator = null): mixed;
/**
* Asks for confirmation.
- *
- * @return bool
*/
- public function confirm(string $question, bool $default = true);
+ public function confirm(string $question, bool $default = true): bool;
/**
* Asks a choice question.
- *
- * @param string|int|null $default
- *
- * @return mixed
*/
- public function choice(string $question, array $choices, $default = null);
+ public function choice(string $question, array $choices, mixed $default = null): mixed;
/**
* Add newline(s).
*/
- public function newLine(int $count = 1);
+ public function newLine(int $count = 1): void;
/**
* Starts the progress output.
*/
- public function progressStart(int $max = 0);
+ public function progressStart(int $max = 0): void;
/**
* Advances the progress output X steps.
*/
- public function progressAdvance(int $step = 1);
+ public function progressAdvance(int $step = 1): void;
/**
* Finishes the progress output.
*/
- public function progressFinish();
+ public function progressFinish(): void;
}
diff --git a/Style/SymfonyStyle.php b/Style/SymfonyStyle.php
index 00edf3882..4cf62cdba 100644
--- a/Style/SymfonyStyle.php
+++ b/Style/SymfonyStyle.php
@@ -15,6 +15,7 @@
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\Helper;
+use Symfony\Component\Console\Helper\OutputWrapper;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
use Symfony\Component\Console\Helper\Table;
@@ -22,6 +23,7 @@
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
+use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\TrimmedBufferOutput;
use Symfony\Component\Console\Question\ChoiceQuestion;
@@ -38,30 +40,27 @@ class SymfonyStyle extends OutputStyle
{
public const MAX_LINE_LENGTH = 120;
- private $input;
- private $output;
- private $questionHelper;
- private $progressBar;
- private $lineLength;
- private $bufferedOutput;
+ private SymfonyQuestionHelper $questionHelper;
+ private ProgressBar $progressBar;
+ private int $lineLength;
+ private TrimmedBufferOutput $bufferedOutput;
- public function __construct(InputInterface $input, OutputInterface $output)
- {
- $this->input = $input;
+ public function __construct(
+ private InputInterface $input,
+ private OutputInterface $output,
+ ) {
$this->bufferedOutput = new TrimmedBufferOutput(\DIRECTORY_SEPARATOR === '\\' ? 4 : 2, $output->getVerbosity(), false, clone $output->getFormatter());
// Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not.
$width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH;
$this->lineLength = min($width - (int) (\DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH);
- parent::__construct($this->output = $output);
+ parent::__construct($output);
}
/**
* Formats a message as a block of text.
- *
- * @param string|array $messages The message to write in the block
*/
- public function block($messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true)
+ public function block(string|array $messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true): void
{
$messages = \is_array($messages) ? array_values($messages) : [$messages];
@@ -70,123 +69,87 @@ public function block($messages, ?string $type = null, ?string $style = null, st
$this->newLine();
}
- /**
- * {@inheritdoc}
- */
- public function title(string $message)
+ public function title(string $message): void
{
$this->autoPrependBlock();
$this->writeln([
- sprintf('%s>', OutputFormatter::escapeTrailingBackslash($message)),
- sprintf('%s>', str_repeat('=', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
+ \sprintf('%s>', OutputFormatter::escapeTrailingBackslash($message)),
+ \sprintf('%s>', str_repeat('=', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
]);
$this->newLine();
}
- /**
- * {@inheritdoc}
- */
- public function section(string $message)
+ public function section(string $message): void
{
$this->autoPrependBlock();
$this->writeln([
- sprintf('%s>', OutputFormatter::escapeTrailingBackslash($message)),
- sprintf('%s>', str_repeat('-', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
+ \sprintf('%s>', OutputFormatter::escapeTrailingBackslash($message)),
+ \sprintf('%s>', str_repeat('-', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
]);
$this->newLine();
}
- /**
- * {@inheritdoc}
- */
- public function listing(array $elements)
+ public function listing(array $elements): void
{
$this->autoPrependText();
- $elements = array_map(function ($element) {
- return sprintf(' * %s', $element);
- }, $elements);
+ $elements = array_map(fn ($element) => \sprintf(' * %s', $element), $elements);
$this->writeln($elements);
$this->newLine();
}
- /**
- * {@inheritdoc}
- */
- public function text($message)
+ public function text(string|array $message): void
{
$this->autoPrependText();
$messages = \is_array($message) ? array_values($message) : [$message];
foreach ($messages as $message) {
- $this->writeln(sprintf(' %s', $message));
+ $this->writeln(\sprintf(' %s', $message));
}
}
/**
* Formats a command comment.
- *
- * @param string|array $message
*/
- public function comment($message)
+ public function comment(string|array $message): void
{
$this->block($message, null, null, ' // >', false, false);
}
- /**
- * {@inheritdoc}
- */
- public function success($message)
+ public function success(string|array $message): void
{
$this->block($message, 'OK', 'fg=black;bg=green', ' ', true);
}
- /**
- * {@inheritdoc}
- */
- public function error($message)
+ public function error(string|array $message): void
{
$this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true);
}
- /**
- * {@inheritdoc}
- */
- public function warning($message)
+ public function warning(string|array $message): void
{
$this->block($message, 'WARNING', 'fg=black;bg=yellow', ' ', true);
}
- /**
- * {@inheritdoc}
- */
- public function note($message)
+ public function note(string|array $message): void
{
$this->block($message, 'NOTE', 'fg=yellow', ' ! ');
}
/**
* Formats an info message.
- *
- * @param string|array $message
*/
- public function info($message)
+ public function info(string|array $message): void
{
$this->block($message, 'INFO', 'fg=green', ' ', true);
}
- /**
- * {@inheritdoc}
- */
- public function caution($message)
+ public function caution(string|array $message): void
{
$this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true);
}
- /**
- * {@inheritdoc}
- */
- public function table(array $headers, array $rows)
+ public function table(array $headers, array $rows): void
{
$this->createTable()
->setHeaders($headers)
@@ -200,7 +163,7 @@ public function table(array $headers, array $rows)
/**
* Formats a horizontal table.
*/
- public function horizontalTable(array $headers, array $rows)
+ public function horizontalTable(array $headers, array $rows): void
{
$this->createTable()
->setHorizontal(true)
@@ -219,10 +182,8 @@ public function horizontalTable(array $headers, array $rows)
* * 'A title'
* * ['key' => 'value']
* * new TableSeparator()
- *
- * @param string|array|TableSeparator ...$list
*/
- public function definitionList(...$list)
+ public function definitionList(string|array|TableSeparator ...$list): void
{
$headers = [];
$row = [];
@@ -247,10 +208,7 @@ public function definitionList(...$list)
$this->horizontalTable($headers, [$row]);
}
- /**
- * {@inheritdoc}
- */
- public function ask(string $question, ?string $default = null, ?callable $validator = null)
+ public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed
{
$question = new Question($question, $default);
$question->setValidator($validator);
@@ -258,10 +216,7 @@ public function ask(string $question, ?string $default = null, ?callable $valida
return $this->askQuestion($question);
}
- /**
- * {@inheritdoc}
- */
- public function askHidden(string $question, ?callable $validator = null)
+ public function askHidden(string $question, ?callable $validator = null): mixed
{
$question = new Question($question);
@@ -271,58 +226,43 @@ public function askHidden(string $question, ?callable $validator = null)
return $this->askQuestion($question);
}
- /**
- * {@inheritdoc}
- */
- public function confirm(string $question, bool $default = true)
+ public function confirm(string $question, bool $default = true): bool
{
return $this->askQuestion(new ConfirmationQuestion($question, $default));
}
- /**
- * {@inheritdoc}
- */
- public function choice(string $question, array $choices, $default = null)
+ public function choice(string $question, array $choices, mixed $default = null, bool $multiSelect = false): mixed
{
if (null !== $default) {
$values = array_flip($choices);
$default = $values[$default] ?? $default;
}
- return $this->askQuestion(new ChoiceQuestion($question, $choices, $default));
+ $questionChoice = new ChoiceQuestion($question, $choices, $default);
+ $questionChoice->setMultiselect($multiSelect);
+
+ return $this->askQuestion($questionChoice);
}
- /**
- * {@inheritdoc}
- */
- public function progressStart(int $max = 0)
+ public function progressStart(int $max = 0): void
{
$this->progressBar = $this->createProgressBar($max);
$this->progressBar->start();
}
- /**
- * {@inheritdoc}
- */
- public function progressAdvance(int $step = 1)
+ public function progressAdvance(int $step = 1): void
{
$this->getProgressBar()->advance($step);
}
- /**
- * {@inheritdoc}
- */
- public function progressFinish()
+ public function progressFinish(): void
{
$this->getProgressBar()->finish();
$this->newLine(2);
- $this->progressBar = null;
+ unset($this->progressBar);
}
- /**
- * {@inheritdoc}
- */
- public function createProgressBar(int $max = 0)
+ public function createProgressBar(int $max = 0): ProgressBar
{
$progressBar = parent::createProgressBar($max);
@@ -337,6 +277,14 @@ public function createProgressBar(int $max = 0)
/**
* @see ProgressBar::iterate()
+ *
+ * @template TKey
+ * @template TValue
+ *
+ * @param iterable $iterable
+ * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
+ *
+ * @return iterable
*/
public function progressIterate(iterable $iterable, ?int $max = null): iterable
{
@@ -345,22 +293,22 @@ public function progressIterate(iterable $iterable, ?int $max = null): iterable
$this->newLine(2);
}
- /**
- * @return mixed
- */
- public function askQuestion(Question $question)
+ public function askQuestion(Question $question): mixed
{
if ($this->input->isInteractive()) {
$this->autoPrependBlock();
}
- if (!$this->questionHelper) {
- $this->questionHelper = new SymfonyQuestionHelper();
- }
+ $this->questionHelper ??= new SymfonyQuestionHelper();
$answer = $this->questionHelper->ask($this->input, $this, $question);
if ($this->input->isInteractive()) {
+ if ($this->output instanceof ConsoleSectionOutput) {
+ // add the new line of the `return` to submit the input to ConsoleSectionOutput, because ConsoleSectionOutput is holding all it's lines.
+ // this is relevant when a `ConsoleSectionOutput::clear` is called.
+ $this->output->addNewLineOfInputSubmit();
+ }
$this->newLine();
$this->bufferedOutput->write("\n");
}
@@ -368,10 +316,7 @@ public function askQuestion(Question $question)
return $answer;
}
- /**
- * {@inheritdoc}
- */
- public function writeln($messages, int $type = self::OUTPUT_NORMAL)
+ public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL): void
{
if (!is_iterable($messages)) {
$messages = [$messages];
@@ -383,10 +328,7 @@ public function writeln($messages, int $type = self::OUTPUT_NORMAL)
}
}
- /**
- * {@inheritdoc}
- */
- public function write($messages, bool $newline = false, int $type = self::OUTPUT_NORMAL)
+ public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL): void
{
if (!is_iterable($messages)) {
$messages = [$messages];
@@ -398,10 +340,7 @@ public function write($messages, bool $newline = false, int $type = self::OUTPUT
}
}
- /**
- * {@inheritdoc}
- */
- public function newLine(int $count = 1)
+ public function newLine(int $count = 1): void
{
parent::newLine($count);
$this->bufferedOutput->write(str_repeat("\n", $count));
@@ -409,10 +348,8 @@ public function newLine(int $count = 1)
/**
* Returns a new instance which makes use of stderr if available.
- *
- * @return self
*/
- public function getErrorStyle()
+ public function getErrorStyle(): self
{
return new self($this->input, $this->getErrorOutput());
}
@@ -428,11 +365,8 @@ public function createTable(): Table
private function getProgressBar(): ProgressBar
{
- if (!$this->progressBar) {
- throw new RuntimeException('The ProgressBar is not started.');
- }
-
- return $this->progressBar;
+ return $this->progressBar
+ ?? throw new RuntimeException('The ProgressBar is not started.');
}
private function autoPrependBlock(): void
@@ -452,7 +386,7 @@ private function autoPrependText(): void
{
$fetched = $this->bufferedOutput->fetch();
// Prepend new line if last char isn't EOL:
- if (!str_ends_with($fetched, "\n")) {
+ if ($fetched && !str_ends_with($fetched, "\n")) {
$this->newLine();
}
}
@@ -470,23 +404,26 @@ private function createBlock(iterable $messages, ?string $type = null, ?string $
$lines = [];
if (null !== $type) {
- $type = sprintf('[%s] ', $type);
- $indentLength = \strlen($type);
+ $type = \sprintf('[%s] ', $type);
+ $indentLength = Helper::width($type);
$lineIndentation = str_repeat(' ', $indentLength);
}
// wrap and add newlines for each element
+ $outputWrapper = new OutputWrapper();
foreach ($messages as $key => $message) {
if ($escape) {
$message = OutputFormatter::escape($message);
}
- $decorationLength = Helper::width($message) - Helper::width(Helper::removeDecoration($this->getFormatter(), $message));
- $messageLineLength = min($this->lineLength - $prefixLength - $indentLength + $decorationLength, $this->lineLength);
- $messageLines = explode(\PHP_EOL, wordwrap($message, $messageLineLength, \PHP_EOL, true));
- foreach ($messageLines as $messageLine) {
- $lines[] = $messageLine;
- }
+ $lines = array_merge(
+ $lines,
+ explode(\PHP_EOL, $outputWrapper->wrap(
+ $message,
+ $this->lineLength - $prefixLength - $indentLength,
+ \PHP_EOL
+ ))
+ );
if (\count($messages) > 1 && $key < \count($messages) - 1) {
$lines[] = '';
@@ -509,7 +446,7 @@ private function createBlock(iterable $messages, ?string $type = null, ?string $
$line .= str_repeat(' ', max($this->lineLength - Helper::width(Helper::removeDecoration($this->getFormatter(), $line)), 0));
if ($style) {
- $line = sprintf('<%s>%s>', $style, $line);
+ $line = \sprintf('<%s>%s>', $style, $line);
}
}
diff --git a/Terminal.php b/Terminal.php
index ee178327a..80f254434 100644
--- a/Terminal.php
+++ b/Terminal.php
@@ -11,18 +11,79 @@
namespace Symfony\Component\Console;
+use Symfony\Component\Console\Output\AnsiColorMode;
+
class Terminal
{
- private static $width;
- private static $height;
- private static $stty;
+ public const DEFAULT_COLOR_MODE = AnsiColorMode::Ansi4;
+
+ private static ?AnsiColorMode $colorMode = null;
+ private static ?int $width = null;
+ private static ?int $height = null;
+ private static ?bool $stty = null;
+
+ /**
+ * About Ansi color types: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+ * For more information about true color support with terminals https://github.com/termstandard/colors/.
+ */
+ public static function getColorMode(): AnsiColorMode
+ {
+ // Use Cache from previous run (or user forced mode)
+ if (null !== self::$colorMode) {
+ return self::$colorMode;
+ }
+
+ // Try with $COLORTERM first
+ if (\is_string($colorterm = getenv('COLORTERM'))) {
+ $colorterm = strtolower($colorterm);
+
+ if (str_contains($colorterm, 'truecolor')) {
+ self::setColorMode(AnsiColorMode::Ansi24);
+
+ return self::$colorMode;
+ }
+
+ if (str_contains($colorterm, '256color')) {
+ self::setColorMode(AnsiColorMode::Ansi8);
+
+ return self::$colorMode;
+ }
+ }
+
+ // Try with $TERM
+ if (\is_string($term = getenv('TERM'))) {
+ $term = strtolower($term);
+
+ if (str_contains($term, 'truecolor')) {
+ self::setColorMode(AnsiColorMode::Ansi24);
+
+ return self::$colorMode;
+ }
+
+ if (str_contains($term, '256color')) {
+ self::setColorMode(AnsiColorMode::Ansi8);
+
+ return self::$colorMode;
+ }
+ }
+
+ self::setColorMode(self::DEFAULT_COLOR_MODE);
+
+ return self::$colorMode;
+ }
+
+ /**
+ * Force a terminal color mode rendering.
+ */
+ public static function setColorMode(?AnsiColorMode $colorMode): void
+ {
+ self::$colorMode = $colorMode;
+ }
/**
* Gets the terminal width.
- *
- * @return int
*/
- public function getWidth()
+ public function getWidth(): int
{
$width = getenv('COLUMNS');
if (false !== $width) {
@@ -38,10 +99,8 @@ public function getWidth()
/**
* Gets the terminal height.
- *
- * @return int
*/
- public function getHeight()
+ public function getHeight(): int
{
$height = getenv('LINES');
if (false !== $height) {
@@ -72,7 +131,7 @@ public static function hasSttyAvailable(): bool
return self::$stty = (bool) shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null'));
}
- private static function initDimensions()
+ private static function initDimensions(): void
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$ansicon = getenv('ANSICON');
@@ -81,7 +140,7 @@ private static function initDimensions()
// or [w, h] from "wxh"
self::$width = (int) $matches[1];
self::$height = isset($matches[4]) ? (int) $matches[4] : (int) $matches[2];
- } elseif (!self::hasVt100Support() && self::hasSttyAvailable()) {
+ } elseif (!sapi_windows_vt100_support(fopen('php://stdout', 'w')) && self::hasSttyAvailable()) {
// only use stty on Windows if the terminal does not support vt100 (e.g. Windows 7 + git-bash)
// testing for stty in a Windows 10 vt100-enabled console will implicitly disable vt100 support on STDOUT
self::initDimensionsUsingStty();
@@ -95,25 +154,17 @@ private static function initDimensions()
}
}
- /**
- * Returns whether STDOUT has vt100 support (some Windows 10+ configurations).
- */
- private static function hasVt100Support(): bool
- {
- return \function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support(fopen('php://stdout', 'w'));
- }
-
/**
* Initializes dimensions using the output of an stty columns line.
*/
- private static function initDimensionsUsingStty()
+ private static function initDimensionsUsingStty(): void
{
if ($sttyString = self::getSttyColumns()) {
- if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) {
+ if (preg_match('/rows.(\d+);.columns.(\d+);/is', $sttyString, $matches)) {
// extract [w, h] from "rows h; columns w;"
self::$width = (int) $matches[2];
self::$height = (int) $matches[1];
- } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) {
+ } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/is', $sttyString, $matches)) {
// extract [w, h] from "; h rows; w columns"
self::$width = (int) $matches[2];
self::$height = (int) $matches[1];
@@ -142,10 +193,10 @@ private static function getConsoleMode(): ?array
*/
private static function getSttyColumns(): ?string
{
- return self::readFromProcess('stty -a | grep columns');
+ return self::readFromProcess(['stty', '-a']);
}
- private static function readFromProcess(string $command): ?string
+ private static function readFromProcess(string|array $command): ?string
{
if (!\function_exists('proc_open')) {
return null;
diff --git a/Tester/ApplicationTester.php b/Tester/ApplicationTester.php
index 3a262e81c..cebb6f8eb 100644
--- a/Tester/ApplicationTester.php
+++ b/Tester/ApplicationTester.php
@@ -28,11 +28,9 @@ class ApplicationTester
{
use TesterTrait;
- private $application;
-
- public function __construct(Application $application)
- {
- $this->application = $application;
+ public function __construct(
+ private Application $application,
+ ) {
}
/**
@@ -47,7 +45,7 @@ public function __construct(Application $application)
*
* @return int The command exit code
*/
- public function run(array $input, array $options = [])
+ public function run(array $input, array $options = []): int
{
$prevShellVerbosity = getenv('SHELL_VERBOSITY');
diff --git a/Tester/CommandCompletionTester.php b/Tester/CommandCompletionTester.php
index ade732752..76cbaf14f 100644
--- a/Tester/CommandCompletionTester.php
+++ b/Tester/CommandCompletionTester.php
@@ -22,11 +22,9 @@
*/
class CommandCompletionTester
{
- private $command;
-
- public function __construct(Command $command)
- {
- $this->command = $command;
+ public function __construct(
+ private Command $command,
+ ) {
}
/**
diff --git a/Tester/CommandTester.php b/Tester/CommandTester.php
index 6c15c25fb..d39cde7f6 100644
--- a/Tester/CommandTester.php
+++ b/Tester/CommandTester.php
@@ -24,11 +24,9 @@ class CommandTester
{
use TesterTrait;
- private $command;
-
- public function __construct(Command $command)
- {
- $this->command = $command;
+ public function __construct(
+ private Command $command,
+ ) {
}
/**
@@ -46,7 +44,7 @@ public function __construct(Command $command)
*
* @return int The command exit code
*/
- public function execute(array $input, array $options = [])
+ public function execute(array $input, array $options = []): int
{
// set the command name automatically if the application requires
// this argument and no command name was passed
diff --git a/Tester/Constraint/CommandIsSuccessful.php b/Tester/Constraint/CommandIsSuccessful.php
index a47324237..d677c27aa 100644
--- a/Tester/Constraint/CommandIsSuccessful.php
+++ b/Tester/Constraint/CommandIsSuccessful.php
@@ -16,33 +16,21 @@
final class CommandIsSuccessful extends Constraint
{
- /**
- * {@inheritdoc}
- */
public function toString(): string
{
return 'is successful';
}
- /**
- * {@inheritdoc}
- */
protected function matches($other): bool
{
return Command::SUCCESS === $other;
}
- /**
- * {@inheritdoc}
- */
protected function failureDescription($other): string
{
return 'the command '.$this->toString();
}
- /**
- * {@inheritdoc}
- */
protected function additionalFailureDescription($other): string
{
$mapping = [
@@ -50,6 +38,6 @@ protected function additionalFailureDescription($other): string
Command::INVALID => 'Command was invalid.',
];
- return $mapping[$other] ?? sprintf('Command returned exit status %d.', $other);
+ return $mapping[$other] ?? \sprintf('Command returned exit status %d.', $other);
}
}
diff --git a/Tester/TesterTrait.php b/Tester/TesterTrait.php
index f454bbf9d..1ab7a70aa 100644
--- a/Tester/TesterTrait.php
+++ b/Tester/TesterTrait.php
@@ -23,25 +23,20 @@
*/
trait TesterTrait
{
- /** @var StreamOutput */
- private $output;
- private $inputs = [];
- private $captureStreamsIndependently = false;
- /** @var InputInterface */
- private $input;
- /** @var int */
- private $statusCode;
+ private StreamOutput $output;
+ private array $inputs = [];
+ private bool $captureStreamsIndependently = false;
+ private InputInterface $input;
+ private int $statusCode;
/**
* Gets the display returned by the last execution of the command or application.
*
- * @return string
- *
* @throws \RuntimeException If it's called before the execute method
*/
- public function getDisplay(bool $normalize = false)
+ public function getDisplay(bool $normalize = false): string
{
- if (null === $this->output) {
+ if (!isset($this->output)) {
throw new \RuntimeException('Output not initialized, did you execute the command before requesting the display?');
}
@@ -60,10 +55,8 @@ public function getDisplay(bool $normalize = false)
* Gets the output written to STDERR by the application.
*
* @param bool $normalize Whether to normalize end of lines to \n or not
- *
- * @return string
*/
- public function getErrorOutput(bool $normalize = false)
+ public function getErrorOutput(bool $normalize = false): string
{
if (!$this->captureStreamsIndependently) {
throw new \LogicException('The error output is not available when the tester is run without "capture_stderr_separately" option set.');
@@ -82,20 +75,16 @@ public function getErrorOutput(bool $normalize = false)
/**
* Gets the input instance used by the last execution of the command or application.
- *
- * @return InputInterface
*/
- public function getInput()
+ public function getInput(): InputInterface
{
return $this->input;
}
/**
* Gets the output instance used by the last execution of the command or application.
- *
- * @return OutputInterface
*/
- public function getOutput()
+ public function getOutput(): OutputInterface
{
return $this->output;
}
@@ -103,17 +92,11 @@ public function getOutput()
/**
* Gets the status code returned by the last execution of the command or application.
*
- * @return int
- *
* @throws \RuntimeException If it's called before the execute method
*/
- public function getStatusCode()
+ public function getStatusCode(): int
{
- if (null === $this->statusCode) {
- throw new \RuntimeException('Status code not initialized, did you execute the command before requesting the status code?');
- }
-
- return $this->statusCode;
+ return $this->statusCode ?? throw new \RuntimeException('Status code not initialized, did you execute the command before requesting the status code?');
}
public function assertCommandIsSuccessful(string $message = ''): void
@@ -129,7 +112,7 @@ public function assertCommandIsSuccessful(string $message = ''): void
*
* @return $this
*/
- public function setInputs(array $inputs)
+ public function setInputs(array $inputs): static
{
$this->inputs = $inputs;
@@ -145,9 +128,9 @@ public function setInputs(array $inputs)
* * verbosity: Sets the output verbosity flag
* * capture_stderr_separately: Make output of stdOut and stdErr separately available
*/
- private function initOutput(array $options)
+ private function initOutput(array $options): void
{
- $this->captureStreamsIndependently = \array_key_exists('capture_stderr_separately', $options) && $options['capture_stderr_separately'];
+ $this->captureStreamsIndependently = $options['capture_stderr_separately'] ?? false;
if (!$this->captureStreamsIndependently) {
$this->output = new StreamOutput(fopen('php://memory', 'w', false));
if (isset($options['decorated'])) {
@@ -169,12 +152,10 @@ private function initOutput(array $options)
$reflectedOutput = new \ReflectionObject($this->output);
$strErrProperty = $reflectedOutput->getProperty('stderr');
- $strErrProperty->setAccessible(true);
$strErrProperty->setValue($this->output, $errorOutput);
$reflectedParent = $reflectedOutput->getParentClass();
$streamProperty = $reflectedParent->getProperty('stream');
- $streamProperty->setAccessible(true);
$streamProperty->setValue($this->output, fopen('php://memory', 'w', false));
}
}
diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php
index d58f28358..4f6e6cb96 100644
--- a/Tests/ApplicationTest.php
+++ b/Tests/ApplicationTest.php
@@ -13,12 +13,16 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\HelpCommand;
use Symfony\Component\Console\Command\LazyCommand;
use Symfony\Component\Console\Command\SignalableCommandInterface;
+use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\CommandLoader\FactoryCommandLoader;
+use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
+use Symfony\Component\Console\Event\ConsoleAlarmEvent;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleSignalEvent;
@@ -50,9 +54,9 @@
class ApplicationTest extends TestCase
{
- protected static $fixturesPath;
+ protected static string $fixturesPath;
- private $colSize;
+ private string|false $colSize;
protected function setUp(): void
{
@@ -67,13 +71,15 @@ protected function tearDown(): void
unset($_SERVER['SHELL_VERBOSITY']);
if (\function_exists('pcntl_signal')) {
+ // We cancel any pending alarms
+ pcntl_alarm(0);
+
// We reset all signals to their default value to avoid side effects
- for ($i = 1; $i <= 15; ++$i) {
- if (9 === $i) {
- continue;
- }
- pcntl_signal($i, SIG_DFL);
- }
+ pcntl_signal(\SIGINT, \SIG_DFL);
+ pcntl_signal(\SIGTERM, \SIG_DFL);
+ pcntl_signal(\SIGUSR1, \SIG_DFL);
+ pcntl_signal(\SIGUSR2, \SIG_DFL);
+ pcntl_signal(\SIGALRM, \SIG_DFL);
}
}
@@ -173,7 +179,7 @@ public function testAllWithCommandLoader()
$this->assertCount(1, $commands, '->all() takes a namespace as its first argument');
$application->setCommandLoader(new FactoryCommandLoader([
- 'foo:bar1' => function () { return new \Foo1Command(); },
+ 'foo:bar1' => fn () => new \Foo1Command(),
]));
$commands = $application->all('foo');
$this->assertCount(2, $commands, '->all() takes a namespace as its first argument');
@@ -227,8 +233,8 @@ public function testAddCommandWithEmptyConstructor()
{
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Command class "Foo5Command" is not correctly initialized. You probably forgot to call the parent constructor.');
- $application = new Application();
- $application->add(new \Foo5Command());
+
+ (new Application())->add(new \Foo5Command());
}
public function testHasGet()
@@ -247,7 +253,6 @@ public function testHasGet()
// simulate --help
$r = new \ReflectionObject($application);
$p = $r->getProperty('wantHelps');
- $p->setAccessible(true);
$p->setValue($application, true);
$command = $application->get('foo:bar');
$this->assertInstanceOf(HelpCommand::class, $command, '->get() returns the help command if --help is provided as the input');
@@ -265,7 +270,7 @@ public function testHasGetWithCommandLoader()
$this->assertEquals($foo, $application->get('afoobar'), '->get() returns a command by alias');
$application->setCommandLoader(new FactoryCommandLoader([
- 'foo:bar1' => function () { return new \Foo1Command(); },
+ 'foo:bar1' => fn () => new \Foo1Command(),
]));
$this->assertTrue($application->has('afoobar'), '->has() returns true if an instance is registered for an alias even with command loader');
@@ -293,8 +298,8 @@ public function testGetInvalidCommand()
{
$this->expectException(CommandNotFoundException::class);
$this->expectExceptionMessage('The command "foofoo" does not exist.');
- $application = new Application();
- $application->get('foofoo');
+
+ (new Application())->get('foofoo');
}
public function testGetNamespaces()
@@ -350,20 +355,21 @@ public function testFindInvalidNamespace()
{
$this->expectException(NamespaceNotFoundException::class);
$this->expectExceptionMessage('There are no commands defined in the "bar" namespace.');
- $application = new Application();
- $application->findNamespace('bar');
+
+ (new Application())->findNamespace('bar');
}
public function testFindUniqueNameButNamespaceName()
{
- $this->expectException(CommandNotFoundException::class);
- $this->expectExceptionMessage('Command "foo1" is not defined');
$application = new Application();
$application->add(new \FooCommand());
$application->add(new \Foo1Command());
$application->add(new \Foo2Command());
- $application->find($commandName = 'foo1');
+ $this->expectException(CommandNotFoundException::class);
+ $this->expectExceptionMessage('Command "foo1" is not defined');
+
+ $application->find('foo1');
}
public function testFind()
@@ -402,20 +408,21 @@ public function testFindCaseInsensitiveAsFallback()
public function testFindCaseInsensitiveSuggestions()
{
- $this->expectException(CommandNotFoundException::class);
- $this->expectExceptionMessage('Command "FoO:BaR" is ambiguous');
$application = new Application();
$application->add(new \FooSameCaseLowercaseCommand());
$application->add(new \FooSameCaseUppercaseCommand());
- $this->assertInstanceOf(\FooSameCaseLowercaseCommand::class, $application->find('FoO:BaR'), '->find() will find two suggestions with case insensitivity');
+ $this->expectException(CommandNotFoundException::class);
+ $this->expectExceptionMessage('Command "FoO:BaR" is ambiguous');
+
+ $application->find('FoO:BaR');
}
public function testFindWithCommandLoader()
{
$application = new Application();
$application->setCommandLoader(new FactoryCommandLoader([
- 'foo:bar' => $f = function () { return new \FooCommand(); },
+ 'foo:bar' => $f = fn () => new \FooCommand(),
]));
$this->assertInstanceOf(\FooCommand::class, $application->find('foo:bar'), '->find() returns a command if its name exists');
@@ -505,10 +512,12 @@ public function testFindCommandWithMissingNamespace()
*/
public function testFindAlternativeExceptionMessageSingle($name)
{
- $this->expectException(CommandNotFoundException::class);
- $this->expectExceptionMessage('Did you mean this');
$application = new Application();
$application->add(new \Foo3Command());
+
+ $this->expectException(CommandNotFoundException::class);
+ $this->expectExceptionMessage('Did you mean this');
+
$application->find($name);
}
@@ -558,6 +567,22 @@ public static function provideInvalidCommandNamesSingle()
];
}
+ public function testRunNamespace()
+ {
+ putenv('COLUMNS=120');
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->add(new \FooCommand());
+ $application->add(new \Foo1Command());
+ $application->add(new \Foo2Command());
+ $tester = new ApplicationTester($application);
+ $tester->run(['command' => 'foo'], ['decorated' => false]);
+ $display = trim($tester->getDisplay(true));
+ $this->assertStringContainsString('Available commands for the "foo" namespace:', $display);
+ $this->assertStringContainsString('The foo:bar command', $display);
+ $this->assertStringContainsString('The foo:bar1 command', $display);
+ }
+
public function testFindAlternativeExceptionMessageMultiple()
{
putenv('COLUMNS=120');
@@ -615,7 +640,7 @@ public function testFindAlternativeCommands()
} catch (\Exception $e) {
$this->assertInstanceOf(CommandNotFoundException::class, $e, '->find() throws a CommandNotFoundException if command does not exist');
$this->assertSame([], $e->getAlternatives());
- $this->assertEquals(sprintf('Command "%s" is not defined.', $commandName), $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, without alternatives');
+ $this->assertEquals(\sprintf('Command "%s" is not defined.', $commandName), $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, without alternatives');
}
// Test if "bar1" command throw a "CommandNotFoundException" and does not contain
@@ -626,7 +651,7 @@ public function testFindAlternativeCommands()
} catch (\Exception $e) {
$this->assertInstanceOf(CommandNotFoundException::class, $e, '->find() throws a CommandNotFoundException if command does not exist');
$this->assertSame(['afoobar1', 'foo:bar1'], $e->getAlternatives());
- $this->assertMatchesRegularExpression(sprintf('/Command "%s" is not defined./', $commandName), $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternatives');
+ $this->assertMatchesRegularExpression(\sprintf('/Command "%s" is not defined./', $commandName), $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternatives');
$this->assertMatchesRegularExpression('/afoobar1/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternative : "afoobar1"');
$this->assertMatchesRegularExpression('/foo:bar1/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, with alternative : "foo:bar1"');
$this->assertDoesNotMatchRegularExpression('/foo:bar(?!1)/', $e->getMessage(), '->find() throws a CommandNotFoundException if command does not exist, without "foo:bar" alternative');
@@ -640,7 +665,7 @@ public function testFindAlternativeCommandsWithAnAlias()
$application = new Application();
$application->setCommandLoader(new FactoryCommandLoader([
- 'foo3' => static function () use ($fooCommand) { return $fooCommand; },
+ 'foo3' => static fn () => $fooCommand,
]));
$application->add($fooCommand);
@@ -727,11 +752,13 @@ public function testFindNamespaceDoesNotFailOnDeepSimilarNamespaces()
public function testFindWithDoubleColonInNameThrowsException()
{
- $this->expectException(CommandNotFoundException::class);
- $this->expectExceptionMessage('Command "foo::bar" is not defined.');
$application = new Application();
$application->add(new \FooCommand());
$application->add(new \Foo4Command());
+
+ $this->expectException(CommandNotFoundException::class);
+ $this->expectExceptionMessage('Command "foo::bar" is not defined.');
+
$application->find('foo::bar');
}
@@ -754,10 +781,15 @@ public function testFindAmbiguousCommandsIfAllAlternativesAreHidden()
$this->assertInstanceOf(\FooCommand::class, $application->find('foo:'));
}
- public function testSetCatchExceptions()
+ /**
+ * @testWith [true]
+ * [false]
+ */
+ public function testSetCatchExceptions(bool $catchErrors)
{
$application = new Application();
$application->setAutoExit(false);
+ $application->setCatchErrors($catchErrors);
putenv('COLUMNS=120');
$tester = new ApplicationTester($application);
@@ -781,6 +813,33 @@ public function testSetCatchExceptions()
}
}
+ /**
+ * @testWith [true]
+ * [false]
+ */
+ public function testSetCatchErrors(bool $catchExceptions)
+ {
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->setCatchExceptions($catchExceptions);
+ $application->add((new Command('boom'))->setCode(fn () => throw new \Error('This is an error.')));
+
+ putenv('COLUMNS=120');
+ $tester = new ApplicationTester($application);
+
+ try {
+ $tester->run(['command' => 'boom']);
+ $this->fail('The exception is not catched.');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(\Error::class, $e);
+ $this->assertSame('This is an error.', $e->getMessage());
+ }
+
+ $application->setCatchErrors(true);
+ $tester->run(['command' => 'boom']);
+ $this->assertStringContainsString(' This is an error.', $tester->getDisplay(true));
+ }
+
public function testAutoExitSetting()
{
$application = new Application();
@@ -797,12 +856,15 @@ public function testRenderException()
putenv('COLUMNS=120');
$tester = new ApplicationTester($application);
- $tester->run(['command' => 'foo'], ['decorated' => false, 'capture_stderr_separately' => true]);
+ $tester->run(['command' => 'foo'], ['decorated' => false, 'verbosity' => Output::VERBOSITY_QUIET, 'capture_stderr_separately' => true]);
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception1.txt', $tester->getErrorOutput(true), '->renderException() renders a pretty exception');
$tester->run(['command' => 'foo'], ['decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE, 'capture_stderr_separately' => true]);
$this->assertStringContainsString('Exception trace', $tester->getErrorOutput(), '->renderException() renders a pretty exception with a stack trace when verbosity is verbose');
+ $tester->run(['command' => 'foo'], ['decorated' => false, 'verbosity' => Output::VERBOSITY_SILENT, 'capture_stderr_separately' => true]);
+ $this->assertSame('', $tester->getErrorOutput(true), '->renderException() renders nothing in SILENT verbosity');
+
$tester->run(['command' => 'list', '--foo' => true], ['decorated' => false, 'capture_stderr_separately' => true]);
$this->assertStringEqualsFile(self::$fixturesPath.'/application_renderexception2.txt', $tester->getErrorOutput(true), '->renderException() renders the command synopsis when an exception occurs in the context of a command');
@@ -901,7 +963,7 @@ public function testRenderAnonymousException()
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
- throw new class('') extends \InvalidArgumentException { };
+ throw new class('') extends \InvalidArgumentException {};
});
$tester = new ApplicationTester($application);
@@ -911,7 +973,7 @@ public function testRenderAnonymousException()
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
- throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', \get_class(new class() { })));
+ throw new \InvalidArgumentException(\sprintf('Dummy type "%s" is invalid.', (new class {})::class));
});
$tester = new ApplicationTester($application);
@@ -927,7 +989,7 @@ public function testRenderExceptionStackTraceContainsRootException()
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
- throw new class('') extends \InvalidArgumentException { };
+ throw new class('') extends \InvalidArgumentException {};
});
$tester = new ApplicationTester($application);
@@ -937,7 +999,7 @@ public function testRenderExceptionStackTraceContainsRootException()
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
- throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', \get_class(new class() { })));
+ throw new \InvalidArgumentException(\sprintf('Dummy type "%s" is invalid.', (new class {})::class));
});
$tester = new ApplicationTester($application);
@@ -1201,8 +1263,6 @@ public function testRunReturnsExitCodeOneForNegativeExceptionCode($exceptionCode
public function testAddingOptionWithDuplicateShortcut()
{
- $this->expectException(\LogicException::class);
- $this->expectExceptionMessage('An option with shortcut "e" already exists.');
$dispatcher = new EventDispatcher();
$application = new Application();
$application->setAutoExit(false);
@@ -1221,6 +1281,9 @@ public function testAddingOptionWithDuplicateShortcut()
$input = new ArrayInput(['command' => 'foo']);
$output = new NullOutput();
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage('An option with shortcut "e" already exists.');
+
$application->run($input, $output);
}
@@ -1229,7 +1292,6 @@ public function testAddingOptionWithDuplicateShortcut()
*/
public function testAddingAlreadySetDefinitionElementData($def)
{
- $this->expectException(\LogicException::class);
$application = new Application();
$application->setAutoExit(false);
$application->setCatchExceptions(false);
@@ -1241,10 +1303,13 @@ public function testAddingAlreadySetDefinitionElementData($def)
$input = new ArrayInput(['command' => 'foo']);
$output = new NullOutput();
+
+ $this->expectException(\LogicException::class);
+
$application->run($input, $output);
}
- public static function getAddingAlreadySetDefinitionElementData()
+ public static function getAddingAlreadySetDefinitionElementData(): array
{
return [
[new InputArgument('command', InputArgument::REQUIRED)],
@@ -1381,8 +1446,6 @@ public function testRunWithDispatcher()
public function testRunWithExceptionAndDispatcher()
{
- $this->expectException(\LogicException::class);
- $this->expectExceptionMessage('error');
$application = new Application();
$application->setDispatcher($this->getDispatcher());
$application->setAutoExit(false);
@@ -1393,6 +1456,10 @@ public function testRunWithExceptionAndDispatcher()
});
$tester = new ApplicationTester($application);
+
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage('error');
+
$tester->run(['command' => 'foo']);
}
@@ -1455,6 +1522,26 @@ public function testRunWithError()
}
}
+ public function testRunWithFindError()
+ {
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->setCatchExceptions(false);
+
+ // Throws an exception when find fails
+ $commandLoader = $this->createMock(CommandLoaderInterface::class);
+ $commandLoader->method('getNames')->willThrowException(new \Error('Find exception'));
+ $application->setCommandLoader($commandLoader);
+
+ // The exception should not be ignored
+ $tester = new ApplicationTester($application);
+
+ $this->expectException(\Error::class);
+ $this->expectExceptionMessage('Find exception');
+
+ $tester->run(['command' => 'foo']);
+ }
+
public function testRunAllowsErrorListenersToSilenceTheException()
{
$dispatcher = $this->getDispatcher();
@@ -1524,8 +1611,6 @@ public function testErrorIsRethrownIfNotHandledByConsoleErrorEvent()
public function testRunWithErrorAndDispatcher()
{
- $this->expectException(\LogicException::class);
- $this->expectExceptionMessage('error');
$application = new Application();
$application->setDispatcher($this->getDispatcher());
$application->setAutoExit(false);
@@ -1538,6 +1623,10 @@ public function testRunWithErrorAndDispatcher()
});
$tester = new ApplicationTester($application);
+
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage('error');
+
$tester->run(['command' => 'dym']);
$this->assertStringContainsString('before.dym.error.after.', $tester->getDisplay(), 'The PHP error did not dispatch events');
}
@@ -1736,23 +1825,25 @@ public function testRunLazyCommandService()
public function testGetDisabledLazyCommand()
{
- $this->expectException(CommandNotFoundException::class);
$application = new Application();
- $application->setCommandLoader(new FactoryCommandLoader(['disabled' => function () { return new DisabledCommand(); }]));
+ $application->setCommandLoader(new FactoryCommandLoader(['disabled' => fn () => new DisabledCommand()]));
+
+ $this->expectException(CommandNotFoundException::class);
+
$application->get('disabled');
}
public function testHasReturnsFalseForDisabledLazyCommand()
{
$application = new Application();
- $application->setCommandLoader(new FactoryCommandLoader(['disabled' => function () { return new DisabledCommand(); }]));
+ $application->setCommandLoader(new FactoryCommandLoader(['disabled' => fn () => new DisabledCommand()]));
$this->assertFalse($application->has('disabled'));
}
public function testAllExcludesDisabledLazyCommand()
{
$application = new Application();
- $application->setCommandLoader(new FactoryCommandLoader(['disabled' => function () { return new DisabledCommand(); }]));
+ $application->setCommandLoader(new FactoryCommandLoader(['disabled' => fn () => new DisabledCommand()]));
$this->assertArrayNotHasKey('disabled', $application->all());
}
@@ -1829,8 +1920,6 @@ public function testErrorIsRethrownIfNotHandledByConsoleErrorEventWithCatchingEn
public function testThrowingErrorListener()
{
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('foo');
$dispatcher = $this->getDispatcher();
$dispatcher->addListener('console.error', function (ConsoleErrorEvent $event) {
throw new \RuntimeException('foo');
@@ -1850,20 +1939,25 @@ public function testThrowingErrorListener()
});
$tester = new ApplicationTester($application);
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('foo');
+
$tester->run(['command' => 'foo']);
}
public function testCommandNameMismatchWithCommandLoaderKeyThrows()
{
- $this->expectException(CommandNotFoundException::class);
- $this->expectExceptionMessage('The "test" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".');
-
$app = new Application();
$loader = new FactoryCommandLoader([
- 'test' => static function () { return new Command('test-command'); },
+ 'test' => static fn () => new Command('test-command'),
]);
$app->setCommandLoader($loader);
+
+ $this->expectException(CommandNotFoundException::class);
+ $this->expectExceptionMessage('The "test" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".');
+
$app->get('test');
}
@@ -1896,7 +1990,8 @@ public function testSignalListener()
$dispatcherCalled = false;
$dispatcher = new EventDispatcher();
- $dispatcher->addListener('console.signal', function () use (&$dispatcherCalled) {
+ $dispatcher->addListener('console.signal', function (ConsoleSignalEvent $e) use (&$dispatcherCalled) {
+ $e->abortExit();
$dispatcherCalled = true;
});
@@ -1945,6 +2040,34 @@ public function testSignalSubscriber()
$this->assertTrue($subscriber2->signaled);
}
+ /**
+ * @requires extension pcntl
+ */
+ public function testSignalDispatchWithoutEventToDispatch()
+ {
+ $command = new SignableCommand();
+
+ $application = $this->createSignalableApplication($command, null);
+ $application->setSignalsToDispatchEvent();
+
+ $this->assertSame(1, $application->run(new ArrayInput(['signal'])));
+ $this->assertTrue($command->signaled);
+ }
+
+ /**
+ * @requires extension pcntl
+ */
+ public function testSignalDispatchWithoutEventDispatcher()
+ {
+ $command = new SignableCommand();
+
+ $application = $this->createSignalableApplication($command, null);
+ $application->setSignalsToDispatchEvent(\SIGUSR1);
+
+ $this->assertSame(1, $application->run(new ArrayInput(['signal'])));
+ $this->assertTrue($command->signaled);
+ }
+
/**
* @requires extension pcntl
*/
@@ -1986,7 +2109,7 @@ public function testSetSignalsToDispatchEvent()
// And now we test without the blank handler
$blankHandlerSignaled = false;
- pcntl_signal(\SIGUSR1, SIG_DFL);
+ pcntl_signal(\SIGUSR1, \SIG_DFL);
$application = $this->createSignalableApplication($command, $dispatcher);
$application->setSignalsToDispatchEvent(\SIGUSR1);
@@ -2030,6 +2153,55 @@ public function testSignalableCommandHandlerCalledAfterEventListener()
$this->assertSame([SignalEventSubscriber::class, SignableCommand::class], $command->signalHandlers);
}
+ public function testSignalableCommandDoesNotInterruptedOnTermSignals()
+ {
+ if (!\defined('SIGINT')) {
+ $this->markTestSkipped('SIGINT not available');
+ }
+
+ $command = new TerminatableCommand(true, \SIGINT);
+ $command->exitCode = 129;
+
+ $dispatcher = new EventDispatcher();
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->setDispatcher($dispatcher);
+ $application->add($command);
+
+ $this->assertSame(129, $application->run(new ArrayInput(['signal'])));
+ }
+
+ public function testSignalableWithEventCommandDoesNotInterruptedOnTermSignals()
+ {
+ if (!\defined('SIGINT')) {
+ $this->markTestSkipped('SIGINT not available');
+ }
+
+ $command = new TerminatableWithEventCommand();
+
+ $terminateEventDispatched = false;
+ $dispatcher = new EventDispatcher();
+ $dispatcher->addSubscriber($command);
+ $dispatcher->addListener('console.terminate', function () use (&$terminateEventDispatched) {
+ $terminateEventDispatched = true;
+ });
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->setDispatcher($dispatcher);
+ $application->add($command);
+ $tester = new ApplicationTester($application);
+ $this->assertSame(51, $tester->run(['signal']));
+ $expected = <<assertSame($expected, $tester->getDisplay(true));
+ $this->assertTrue($terminateEventDispatched);
+ }
+
/**
* @group tty
*/
@@ -2063,6 +2235,168 @@ public function testSignalableRestoresStty()
$this->assertSame($previousSttyMode, $sttyMode);
}
+ /**
+ * @requires extension pcntl
+ */
+ public function testAlarmSubscriberNotCalledByDefault()
+ {
+ $command = new BaseSignableCommand(false);
+
+ $subscriber = new AlarmEventSubscriber();
+
+ $dispatcher = new EventDispatcher();
+ $dispatcher->addSubscriber($subscriber);
+
+ $application = $this->createSignalableApplication($command, $dispatcher);
+
+ $this->assertSame(0, $application->run(new ArrayInput(['signal'])));
+ $this->assertFalse($subscriber->signaled);
+ }
+
+ /**
+ * @requires extension pcntl
+ */
+ public function testAlarmSubscriberNotCalledForOtherSignals()
+ {
+ $command = new SignableCommand();
+
+ $subscriber1 = new SignalEventSubscriber();
+ $subscriber2 = new AlarmEventSubscriber();
+
+ $dispatcher = new EventDispatcher();
+ $dispatcher->addSubscriber($subscriber1);
+ $dispatcher->addSubscriber($subscriber2);
+
+ $application = $this->createSignalableApplication($command, $dispatcher);
+
+ $this->assertSame(1, $application->run(new ArrayInput(['signal'])));
+ $this->assertTrue($subscriber1->signaled);
+ $this->assertFalse($subscriber2->signaled);
+ }
+
+ /**
+ * @requires extension pcntl
+ */
+ public function testAlarmSubscriber()
+ {
+ $command = new BaseSignableCommand(signal: \SIGALRM);
+
+ $subscriber1 = new AlarmEventSubscriber();
+ $subscriber2 = new AlarmEventSubscriber();
+
+ $dispatcher = new EventDispatcher();
+ $dispatcher->addSubscriber($subscriber1);
+ $dispatcher->addSubscriber($subscriber2);
+
+ $application = $this->createSignalableApplication($command, $dispatcher);
+
+ $this->assertSame(1, $application->run(new ArrayInput(['signal'])));
+ $this->assertTrue($subscriber1->signaled);
+ $this->assertTrue($subscriber2->signaled);
+ }
+
+ /**
+ * @requires extension pcntl
+ */
+ public function testAlarmDispatchWithoutEventDispatcher()
+ {
+ $command = new AlarmableCommand(1);
+ $command->loop = 11000;
+
+ $application = $this->createSignalableApplication($command, null);
+
+ $this->assertSame(1, $application->run(new ArrayInput(['alarm'])));
+ $this->assertSame(1, $application->getAlarmInterval());
+ $this->assertTrue($command->signaled);
+ }
+
+ /**
+ * @requires extension pcntl
+ */
+ public function testAlarmableCommandWithoutInterval()
+ {
+ $command = new AlarmableCommand(0);
+ $command->loop = 11000;
+
+ $dispatcher = new EventDispatcher();
+
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->setDispatcher($dispatcher);
+ $application->add($command);
+
+ $this->assertSame(0, $application->run(new ArrayInput(['alarm'])));
+ $this->assertFalse($command->signaled);
+ }
+
+ /**
+ * @requires extension pcntl
+ */
+ public function testAlarmableCommandHandlerCalledAfterEventListener()
+ {
+ $command = new AlarmableCommand(1);
+ $command->loop = 11000;
+
+ $subscriber = new AlarmEventSubscriber();
+
+ $dispatcher = new EventDispatcher();
+ $dispatcher->addSubscriber($subscriber);
+
+ $application = $this->createSignalableApplication($command, $dispatcher);
+
+ $this->assertSame(1, $application->run(new ArrayInput(['alarm'])));
+ $this->assertSame([AlarmEventSubscriber::class, AlarmableCommand::class], $command->signalHandlers);
+ }
+
+ /**
+ * @requires extension pcntl
+ *
+ * @testWith [false]
+ * [4]
+ */
+ public function testAlarmSubscriberCalledAfterSignalSubscriberAndInheritsExitCode(int|false $exitCode)
+ {
+ $command = new BaseSignableCommand(signal: \SIGALRM);
+
+ $subscriber1 = new class($exitCode) extends SignalEventSubscriber {
+ public function __construct(private int|false $exitCode)
+ {
+ }
+
+ public function onSignal(ConsoleSignalEvent $event): void
+ {
+ parent::onSignal($event);
+
+ if (false === $this->exitCode) {
+ $event->abortExit();
+ } else {
+ $event->setExitCode($this->exitCode);
+ }
+ }
+ };
+ $subscriber2 = new class($exitCode) extends AlarmEventSubscriber {
+ public function __construct(private int|false $exitCode)
+ {
+ }
+
+ public function onAlarm(ConsoleAlarmEvent $event): void
+ {
+ TestCase::assertSame($this->exitCode, $event->getExitCode());
+
+ parent::onAlarm($event);
+ }
+ };
+
+ $dispatcher = new EventDispatcher();
+ $dispatcher->addSubscriber($subscriber1);
+ $dispatcher->addSubscriber($subscriber2);
+
+ $application = $this->createSignalableApplication($command, $dispatcher);
+
+ $this->assertSame(1, $application->run(new ArrayInput(['signal'])));
+ $this->assertSame([SignalEventSubscriber::class, AlarmEventSubscriber::class], $command->signalHandlers);
+ }
+
private function createSignalableApplication(Command $command, ?EventDispatcherInterface $dispatcher): Application
{
$application = new Application();
@@ -2070,7 +2404,7 @@ private function createSignalableApplication(Command $command, ?EventDispatcherI
if ($dispatcher) {
$application->setDispatcher($dispatcher);
}
- $application->add(new LazyCommand('signal', [], '', false, function () use ($command) { return $command; }, true));
+ $application->add(new LazyCommand($command::getDefaultName(), [], '', false, fn () => $command, true));
return $application;
}
@@ -2097,9 +2431,6 @@ protected function getDefaultHelperSet(): HelperSet
class CustomDefaultCommandApplication extends Application
{
- /**
- * Overwrites the constructor in order to set a different default command.
- */
public function __construct()
{
parent::__construct();
@@ -2128,31 +2459,33 @@ public function isEnabled(): bool
}
}
+#[AsCommand(name: 'signal')]
class BaseSignableCommand extends Command
{
- public $signaled = false;
- public $signalHandlers = [];
- public $loop = 1000;
- private $emitsSignal;
-
- protected static $defaultName = 'signal';
+ public bool $signaled = false;
+ public int $exitCode = 1;
+ public array $signalHandlers = [];
+ public int $loop = 1000;
+ private bool $emitsSignal;
+ private int $signal;
- public function __construct(bool $emitsSignal = true)
+ public function __construct(bool $emitsSignal = true, int $signal = \SIGUSR1)
{
parent::__construct();
$this->emitsSignal = $emitsSignal;
+ $this->signal = $signal;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($this->emitsSignal) {
- posix_kill(posix_getpid(), \SIGUSR1);
+ posix_kill(posix_getpid(), $this->signal);
}
for ($i = 0; $i < $this->loop; ++$i) {
usleep(100);
if ($this->signaled) {
- return 1;
+ return $this->exitCode;
}
}
@@ -2160,31 +2493,100 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
}
+#[AsCommand(name: 'signal')]
class SignableCommand extends BaseSignableCommand implements SignalableCommandInterface
{
- protected static $defaultName = 'signal';
-
public function getSubscribedSignals(): array
{
return SignalRegistry::isSupported() ? [\SIGUSR1] : [];
}
- public function handleSignal(int $signal): void
+ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
{
$this->signaled = true;
$this->signalHandlers[] = __CLASS__;
+
+ return false;
+ }
+}
+
+#[AsCommand(name: 'signal')]
+class TerminatableCommand extends BaseSignableCommand implements SignalableCommandInterface
+{
+ public function getSubscribedSignals(): array
+ {
+ return SignalRegistry::isSupported() ? [\SIGINT] : [];
+ }
+
+ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
+ {
+ $this->signaled = true;
+ $this->signalHandlers[] = __CLASS__;
+
+ return false;
+ }
+}
+
+#[AsCommand(name: 'signal')]
+class TerminatableWithEventCommand extends Command implements SignalableCommandInterface, EventSubscriberInterface
+{
+ private bool $shouldContinue = true;
+ private OutputInterface $output;
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->output = $output;
+
+ for ($i = 0; $i <= 10 && $this->shouldContinue; ++$i) {
+ $output->writeln('Still processing...');
+ posix_kill(posix_getpid(), \SIGINT);
+ }
+
+ $output->writeln('Wrapping up, wait a sec...');
+
+ return 51;
+ }
+
+ public function getSubscribedSignals(): array
+ {
+ return [\SIGINT];
+ }
+
+ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
+ {
+ $this->shouldContinue = false;
+
+ $this->output->writeln(json_encode(['exit code', $signal, $previousExitCode]));
+
+ return false;
+ }
+
+ public function handleSignalEvent(ConsoleSignalEvent $event): void
+ {
+ $this->output->writeln(json_encode(['handling event', $event->getHandlingSignal(), $event->getExitCode()]));
+
+ $event->setExitCode(125);
+ }
+
+ public static function getSubscribedEvents(): array
+ {
+ return [
+ ConsoleEvents::SIGNAL => 'handleSignalEvent',
+ ];
}
}
class SignalEventSubscriber implements EventSubscriberInterface
{
- public $signaled = false;
+ public bool $signaled = false;
public function onSignal(ConsoleSignalEvent $event): void
{
$this->signaled = true;
$event->getCommand()->signaled = true;
$event->getCommand()->signalHandlers[] = __CLASS__;
+
+ $event->abortExit();
}
public static function getSubscribedEvents(): array
@@ -2192,3 +2594,51 @@ public static function getSubscribedEvents(): array
return ['console.signal' => 'onSignal'];
}
}
+
+#[AsCommand(name: 'alarm')]
+class AlarmableCommand extends BaseSignableCommand implements SignalableCommandInterface
+{
+ public function __construct(private int $alarmInterval)
+ {
+ parent::__construct(false);
+ }
+
+ protected function initialize(InputInterface $input, OutputInterface $output): void
+ {
+ $this->getApplication()->setAlarmInterval($this->alarmInterval);
+ }
+
+ public function getSubscribedSignals(): array
+ {
+ return [\SIGALRM];
+ }
+
+ public function handleSignal(int $signal, false|int $previousExitCode = 0): int|false
+ {
+ if (\SIGALRM === $signal) {
+ $this->signaled = true;
+ $this->signalHandlers[] = __CLASS__;
+ }
+
+ return false;
+ }
+}
+
+class AlarmEventSubscriber implements EventSubscriberInterface
+{
+ public bool $signaled = false;
+
+ public function onAlarm(ConsoleAlarmEvent $event): void
+ {
+ $this->signaled = true;
+ $event->getCommand()->signaled = true;
+ $event->getCommand()->signalHandlers[] = __CLASS__;
+
+ $event->abortExit();
+ }
+
+ public static function getSubscribedEvents(): array
+ {
+ return [ConsoleAlarmEvent::class => 'onAlarm'];
+ }
+}
diff --git a/Tests/ColorTest.php b/Tests/ColorTest.php
index c9615aa8d..2a47f73f5 100644
--- a/Tests/ColorTest.php
+++ b/Tests/ColorTest.php
@@ -13,10 +13,12 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Color;
+use Symfony\Component\Console\Output\AnsiColorMode;
+use Symfony\Component\Console\Terminal;
class ColorTest extends TestCase
{
- public function testAnsiColors()
+ public function testAnsi4Colors()
{
$color = new Color();
$this->assertSame(' ', $color->apply(' '));
@@ -33,21 +35,22 @@ public function testAnsiColors()
public function testTrueColors()
{
- if ('truecolor' !== getenv('COLORTERM')) {
- $this->markTestSkipped('True color not supported.');
- }
+ Terminal::setColorMode(AnsiColorMode::Ansi24);
- $color = new Color('#fff', '#000');
- $this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' '));
+ try {
+ $color = new Color('#fff', '#000');
+ $this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' '));
- $color = new Color('#ffffff', '#000000');
- $this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' '));
+ $color = new Color('#ffffff', '#000000');
+ $this->assertSame("\033[38;2;255;255;255;48;2;0;0;0m \033[39;49m", $color->apply(' '));
+ } finally {
+ Terminal::setColorMode(null);
+ }
}
- public function testDegradedTrueColors()
+ public function testDegradedTrueColorsToAnsi4()
{
- $colorterm = getenv('COLORTERM');
- putenv('COLORTERM=');
+ Terminal::setColorMode(AnsiColorMode::Ansi4);
try {
$color = new Color('#f00', '#ff0');
@@ -56,7 +59,22 @@ public function testDegradedTrueColors()
$color = new Color('#c0392b', '#f1c40f');
$this->assertSame("\033[31;43m \033[39;49m", $color->apply(' '));
} finally {
- putenv('COLORTERM='.$colorterm);
+ Terminal::setColorMode(null);
+ }
+ }
+
+ public function testDegradedTrueColorsToAnsi8()
+ {
+ Terminal::setColorMode(AnsiColorMode::Ansi8);
+
+ try {
+ $color = new Color('#f57255', '#8993c0');
+ $this->assertSame("\033[38;5;210;48;5;146m \033[39;49m", $color->apply(' '));
+
+ $color = new Color('#000000', '#ffffff');
+ $this->assertSame("\033[38;5;16;48;5;231m \033[39;49m", $color->apply(' '));
+ } finally {
+ Terminal::setColorMode(null);
}
}
}
diff --git a/Tests/Command/CommandTest.php b/Tests/Command/CommandTest.php
index 63a5ea853..199c0c309 100644
--- a/Tests/Command/CommandTest.php
+++ b/Tests/Command/CommandTest.php
@@ -28,7 +28,7 @@
class CommandTest extends TestCase
{
- protected static $fixturesPath;
+ protected static string $fixturesPath;
public static function setUpBeforeClass(): void
{
@@ -85,6 +85,16 @@ public function testAddArgument()
$this->assertTrue($command->getDefinition()->hasArgument('foo'), '->addArgument() adds an argument to the command');
}
+ public function testAddArgumentFull()
+ {
+ $command = new \TestCommand();
+ $command->addArgument('foo', InputArgument::OPTIONAL, 'Description', 'default', ['a', 'b']);
+ $argument = $command->getDefinition()->getArgument('foo');
+ $this->assertSame('Description', $argument->getDescription());
+ $this->assertSame('default', $argument->getDefault());
+ $this->assertTrue($argument->hasCompletion());
+ }
+
public function testAddOption()
{
$command = new \TestCommand();
@@ -93,10 +103,21 @@ public function testAddOption()
$this->assertTrue($command->getDefinition()->hasOption('foo'), '->addOption() adds an option to the command');
}
+ public function testAddOptionFull()
+ {
+ $command = new \TestCommand();
+ $command->addOption('foo', ['f'], InputOption::VALUE_OPTIONAL, 'Description', 'default', ['a', 'b']);
+ $option = $command->getDefinition()->getOption('foo');
+ $this->assertSame('f', $option->getShortcut());
+ $this->assertSame('Description', $option->getDescription());
+ $this->assertSame('default', $option->getDefault());
+ $this->assertTrue($option->hasCompletion());
+ }
+
public function testSetHidden()
{
$command = new \TestCommand();
- $command->setHidden(true);
+ $command->setHidden();
$this->assertTrue($command->isHidden());
}
@@ -118,13 +139,12 @@ public function testGetNamespaceGetNameSetName()
public function testInvalidCommandNames($name)
{
$this->expectException(\InvalidArgumentException::class);
- $this->expectExceptionMessage(sprintf('Command name "%s" is invalid.', $name));
+ $this->expectExceptionMessage(\sprintf('Command name "%s" is invalid.', $name));
- $command = new \TestCommand();
- $command->setName($name);
+ (new \TestCommand())->setName($name);
}
- public static function provideInvalidCommandNames()
+ public static function provideInvalidCommandNames(): array
{
return [
[''],
@@ -212,8 +232,7 @@ public function testGetHelperWithoutHelperSet()
{
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot retrieve helper "formatter" because there is no HelperSet defined.');
- $command = new \TestCommand();
- $command->getHelper('formatter');
+ (new \TestCommand())->getHelper('formatter');
}
public function testMergeApplicationDefinition()
@@ -227,7 +246,6 @@ public function testMergeApplicationDefinition()
$r = new \ReflectionObject($command);
$m = $r->getMethod('mergeApplicationDefinition');
- $m->setAccessible(true);
$m->invoke($command);
$this->assertTrue($command->getDefinition()->hasArgument('foo'), '->mergeApplicationDefinition() merges the application arguments and the command arguments');
$this->assertTrue($command->getDefinition()->hasArgument('bar'), '->mergeApplicationDefinition() merges the application arguments and the command arguments');
@@ -249,7 +267,6 @@ public function testMergeApplicationDefinitionWithoutArgsThenWithArgsAddsArgs()
$r = new \ReflectionObject($command);
$m = $r->getMethod('mergeApplicationDefinition');
- $m->setAccessible(true);
$m->invoke($command, false);
$this->assertTrue($command->getDefinition()->hasOption('bar'), '->mergeApplicationDefinition(false) merges the application and the command options');
$this->assertFalse($command->getDefinition()->hasArgument('foo'), '->mergeApplicationDefinition(false) does not merge the application arguments');
@@ -283,16 +300,17 @@ public function testExecuteMethodNeedsToBeOverridden()
{
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('You must override the execute() method in the concrete command class.');
- $command = new Command('foo');
- $command->run(new StringInput(''), new NullOutput());
+ (new Command('foo'))->run(new StringInput(''), new NullOutput());
}
public function testRunWithInvalidOption()
{
- $this->expectException(InvalidOptionException::class);
- $this->expectExceptionMessage('The "--bar" option does not exist.');
$command = new \TestCommand();
$tester = new CommandTester($command);
+
+ $this->expectException(InvalidOptionException::class);
+ $this->expectExceptionMessage('The "--bar" option does not exist.');
+
$tester->execute(['--bar' => true]);
}
@@ -383,7 +401,7 @@ private static function createClosure()
public function testSetCodeWithNonClosureCallable()
{
$command = new \TestCommand();
- $ret = $command->setCode([$this, 'callableMethodCommand']);
+ $ret = $command->setCode($this->callableMethodCommand(...));
$this->assertEquals($command, $ret, '->setCode() implements a fluent interface');
$tester = new CommandTester($command);
$tester->execute([]);
@@ -407,9 +425,6 @@ public function testSetCodeWithStaticAnonymousFunction()
$this->assertEquals('interact called'.\PHP_EOL.'not bound'.\PHP_EOL, $tester->getDisplay());
}
- /**
- * @requires PHP 8
- */
public function testCommandAttribute()
{
$this->assertSame('|foo|f', Php8Command::getDefaultName());
@@ -423,21 +438,27 @@ public function testCommandAttribute()
$this->assertSame(['f'], $command->getAliases());
}
- /**
- * @requires PHP 8
- */
+ public function testAttributeOverridesProperty()
+ {
+ $this->assertSame('my:command', MyAnnotatedCommand::getDefaultName());
+ $this->assertSame('This is a command I wrote all by myself', MyAnnotatedCommand::getDefaultDescription());
+
+ $command = new MyAnnotatedCommand();
+
+ $this->assertSame('my:command', $command->getName());
+ $this->assertSame('This is a command I wrote all by myself', $command->getDescription());
+ }
+
public function testDefaultCommand()
{
$apl = new Application();
$apl->setDefaultCommand(Php8Command::getDefaultName());
$property = new \ReflectionProperty($apl, 'defaultCommand');
- $property->setAccessible(true);
$this->assertEquals('foo', $property->getValue($apl));
$apl->setDefaultCommand(Php8Command2::getDefaultName());
$property = new \ReflectionProperty($apl, 'defaultCommand');
- $property->setAccessible(true);
$this->assertEquals('foo2', $property->getValue($apl));
}
@@ -461,3 +482,11 @@ class Php8Command extends Command
class Php8Command2 extends Command
{
}
+
+#[AsCommand(name: 'my:command', description: 'This is a command I wrote all by myself')]
+class MyAnnotatedCommand extends Command
+{
+ protected static $defaultName = 'i-shall-be-ignored';
+
+ protected static $defaultDescription = 'This description should be ignored.';
+}
diff --git a/Tests/Command/CompleteCommandTest.php b/Tests/Command/CompleteCommandTest.php
index 30b22c967..75519eb49 100644
--- a/Tests/Command/CompleteCommandTest.php
+++ b/Tests/Command/CompleteCommandTest.php
@@ -24,9 +24,9 @@
class CompleteCommandTest extends TestCase
{
- private $command;
- private $application;
- private $tester;
+ private CompleteCommand $command;
+ private Application $application;
+ private CommandTester $tester;
protected function setUp(): void
{
@@ -47,12 +47,14 @@ public function testRequiredShellOption()
public function testUnsupportedShellOption()
{
- $this->expectExceptionMessage('Shell completion is not supported for your shell: "unsupported" (supported: "bash").');
+ $this->expectExceptionMessage('Shell completion is not supported for your shell: "unsupported" (supported: "bash", "fish", "zsh").');
$this->execute(['--shell' => 'unsupported']);
}
public function testAdditionalShellSupport()
{
+ $this->expectNotToPerformAssertions();
+
$this->command = new CompleteCommand(['supported' => BashCompletionOutput::class]);
$this->command->setApplication($this->application);
$this->tester = new CommandTester($this->command);
@@ -61,8 +63,6 @@ public function testAdditionalShellSupport()
// verify that the default set of shells is still supported
$this->execute(['--shell' => 'bash', '--current' => '1', '--input' => ['bin/console']]);
-
- $this->assertTrue(true);
}
/**
@@ -119,16 +119,16 @@ public function testCompleteCommandInputDefinition(array $input, array $suggesti
public static function provideCompleteCommandInputDefinitionInputs()
{
- yield 'definition' => [['bin/console', 'hello', '-'], ['--help', '--quiet', '--verbose', '--version', '--ansi', '--no-ansi', '--no-interaction']];
+ yield 'definition' => [['bin/console', 'hello', '-'], ['--help', '--silent', '--quiet', '--verbose', '--version', '--ansi', '--no-ansi', '--no-interaction']];
yield 'custom' => [['bin/console', 'hello'], ['Fabien', 'Robin', 'Wouter']];
- yield 'definition-aliased' => [['bin/console', 'ahoy', '-'], ['--help', '--quiet', '--verbose', '--version', '--ansi', '--no-ansi', '--no-interaction']];
+ yield 'definition-aliased' => [['bin/console', 'ahoy', '-'], ['--help', '--silent', '--quiet', '--verbose', '--version', '--ansi', '--no-ansi', '--no-interaction']];
yield 'custom-aliased' => [['bin/console', 'ahoy'], ['Fabien', 'Robin', 'Wouter']];
}
private function execute(array $input)
{
// run in verbose mode to assert exceptions
- $this->tester->execute($input ? ($input + ['--shell' => 'bash']) : $input, ['verbosity' => OutputInterface::VERBOSITY_DEBUG]);
+ $this->tester->execute($input ? ($input + ['--shell' => 'bash', '--api-version' => CompleteCommand::COMPLETION_API_VERSION]) : $input, ['verbosity' => OutputInterface::VERBOSITY_DEBUG]);
}
}
@@ -138,6 +138,7 @@ public function configure(): void
{
$this->setName('hello')
->setAliases(['ahoy'])
+ ->setDescription('Hello test command')
->addArgument('name', InputArgument::REQUIRED)
;
}
diff --git a/Tests/Command/DumpCompletionCommandTest.php b/Tests/Command/DumpCompletionCommandTest.php
index 8d0fb4553..ba23bb331 100644
--- a/Tests/Command/DumpCompletionCommandTest.php
+++ b/Tests/Command/DumpCompletionCommandTest.php
@@ -32,7 +32,7 @@ public static function provideCompletionSuggestions()
{
yield 'shell' => [
[''],
- ['bash'],
+ ['bash', 'fish', 'zsh'],
];
}
}
diff --git a/Tests/Command/HelpCommandTest.php b/Tests/Command/HelpCommandTest.php
index d61c912fc..c36ab62df 100644
--- a/Tests/Command/HelpCommandTest.php
+++ b/Tests/Command/HelpCommandTest.php
@@ -87,7 +87,7 @@ public static function provideCompletionSuggestions()
{
yield 'option --format' => [
['--format', ''],
- ['txt', 'xml', 'json', 'md'],
+ ['txt', 'xml', 'json', 'md', 'rst'],
];
yield 'nothing' => [
diff --git a/Tests/Command/ListCommandTest.php b/Tests/Command/ListCommandTest.php
index 7ed9d3d5d..a6ffc8ab5 100644
--- a/Tests/Command/ListCommandTest.php
+++ b/Tests/Command/ListCommandTest.php
@@ -80,7 +80,8 @@ public function testExecuteListsCommandsOrder()
Options:
-h, --help Display help for the given command. When no command is given display help for the list command
- -q, --quiet Do not output any message
+ --silent Do not output any message
+ -q, --quiet Only errors are displayed. All other output is suppressed
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
@@ -131,7 +132,7 @@ public static function provideCompletionSuggestions()
{
yield 'option --format' => [
['--format', ''],
- ['txt', 'xml', 'json', 'md'],
+ ['txt', 'xml', 'json', 'md', 'rst'],
];
yield 'namespace' => [
diff --git a/Tests/Command/LockableTraitTest.php b/Tests/Command/LockableTraitTest.php
index 59ddf3595..0268d9681 100644
--- a/Tests/Command/LockableTraitTest.php
+++ b/Tests/Command/LockableTraitTest.php
@@ -14,18 +14,20 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Lock\LockFactory;
+use Symfony\Component\Lock\SharedLockInterface;
use Symfony\Component\Lock\Store\FlockStore;
use Symfony\Component\Lock\Store\SemaphoreStore;
class LockableTraitTest extends TestCase
{
- protected static $fixturesPath;
+ protected static string $fixturesPath;
public static function setUpBeforeClass(): void
{
self::$fixturesPath = __DIR__.'/../Fixtures/';
require_once self::$fixturesPath.'/FooLockCommand.php';
require_once self::$fixturesPath.'/FooLock2Command.php';
+ require_once self::$fixturesPath.'/FooLock3Command.php';
}
public function testLockIsReleased()
@@ -64,4 +66,18 @@ public function testMultipleLockCallsThrowLogicException()
$tester = new CommandTester($command);
$this->assertSame(1, $tester->execute([]));
}
+
+ public function testCustomLockFactoryIsUsed()
+ {
+ $lockFactory = $this->createMock(LockFactory::class);
+ $command = new \FooLock3Command($lockFactory);
+
+ $tester = new CommandTester($command);
+
+ $lock = $this->createMock(SharedLockInterface::class);
+ $lock->method('acquire')->willReturn(false);
+
+ $lockFactory->expects(static::once())->method('createLock')->willReturn($lock);
+ $this->assertSame(1, $tester->execute([]));
+ }
}
diff --git a/Tests/Command/SingleCommandApplicationTest.php b/Tests/Command/SingleCommandApplicationTest.php
index 8fae4876b..98000c0a7 100644
--- a/Tests/Command/SingleCommandApplicationTest.php
+++ b/Tests/Command/SingleCommandApplicationTest.php
@@ -21,7 +21,7 @@ class SingleCommandApplicationTest extends TestCase
{
public function testRun()
{
- $command = new class() extends SingleCommandApplication {
+ $command = new class extends SingleCommandApplication {
protected function execute(InputInterface $input, OutputInterface $output): int
{
return 0;
diff --git a/Tests/CommandLoader/ContainerCommandLoaderTest.php b/Tests/CommandLoader/ContainerCommandLoaderTest.php
index e7f138933..f2049ef5c 100644
--- a/Tests/CommandLoader/ContainerCommandLoaderTest.php
+++ b/Tests/CommandLoader/ContainerCommandLoaderTest.php
@@ -22,8 +22,8 @@ class ContainerCommandLoaderTest extends TestCase
public function testHas()
{
$loader = new ContainerCommandLoader(new ServiceLocator([
- 'foo-service' => function () { return new Command('foo'); },
- 'bar-service' => function () { return new Command('bar'); },
+ 'foo-service' => fn () => new Command('foo'),
+ 'bar-service' => fn () => new Command('bar'),
]), ['foo' => 'foo-service', 'bar' => 'bar-service']);
$this->assertTrue($loader->has('foo'));
@@ -34,8 +34,8 @@ public function testHas()
public function testGet()
{
$loader = new ContainerCommandLoader(new ServiceLocator([
- 'foo-service' => function () { return new Command('foo'); },
- 'bar-service' => function () { return new Command('bar'); },
+ 'foo-service' => fn () => new Command('foo'),
+ 'bar-service' => fn () => new Command('bar'),
]), ['foo' => 'foo-service', 'bar' => 'bar-service']);
$this->assertInstanceOf(Command::class, $loader->get('foo'));
@@ -51,8 +51,8 @@ public function testGetUnknownCommandThrows()
public function testGetCommandNames()
{
$loader = new ContainerCommandLoader(new ServiceLocator([
- 'foo-service' => function () { return new Command('foo'); },
- 'bar-service' => function () { return new Command('bar'); },
+ 'foo-service' => fn () => new Command('foo'),
+ 'bar-service' => fn () => new Command('bar'),
]), ['foo' => 'foo-service', 'bar' => 'bar-service']);
$this->assertSame(['foo', 'bar'], $loader->getNames());
diff --git a/Tests/CommandLoader/FactoryCommandLoaderTest.php b/Tests/CommandLoader/FactoryCommandLoaderTest.php
index aebb429e6..148806413 100644
--- a/Tests/CommandLoader/FactoryCommandLoaderTest.php
+++ b/Tests/CommandLoader/FactoryCommandLoaderTest.php
@@ -21,8 +21,8 @@ class FactoryCommandLoaderTest extends TestCase
public function testHas()
{
$loader = new FactoryCommandLoader([
- 'foo' => function () { return new Command('foo'); },
- 'bar' => function () { return new Command('bar'); },
+ 'foo' => fn () => new Command('foo'),
+ 'bar' => fn () => new Command('bar'),
]);
$this->assertTrue($loader->has('foo'));
@@ -33,8 +33,8 @@ public function testHas()
public function testGet()
{
$loader = new FactoryCommandLoader([
- 'foo' => function () { return new Command('foo'); },
- 'bar' => function () { return new Command('bar'); },
+ 'foo' => fn () => new Command('foo'),
+ 'bar' => fn () => new Command('bar'),
]);
$this->assertInstanceOf(Command::class, $loader->get('foo'));
@@ -50,8 +50,8 @@ public function testGetUnknownCommandThrows()
public function testGetCommandNames()
{
$loader = new FactoryCommandLoader([
- 'foo' => function () { return new Command('foo'); },
- 'bar' => function () { return new Command('bar'); },
+ 'foo' => fn () => new Command('foo'),
+ 'bar' => fn () => new Command('bar'),
]);
$this->assertSame(['foo', 'bar'], $loader->getNames());
diff --git a/Tests/Completion/CompletionInputTest.php b/Tests/Completion/CompletionInputTest.php
index 65708d3ec..df0d081fd 100644
--- a/Tests/Completion/CompletionInputTest.php
+++ b/Tests/Completion/CompletionInputTest.php
@@ -119,7 +119,6 @@ public function testFromString($inputStr, array $expectedTokens)
$input = CompletionInput::fromString($inputStr, 1);
$tokensProperty = (new \ReflectionClass($input))->getProperty('tokens');
- $tokensProperty->setAccessible(true);
$this->assertEquals($expectedTokens, $tokensProperty->getValue($input));
}
diff --git a/Tests/Completion/Output/CompletionOutputTestCase.php b/Tests/Completion/Output/CompletionOutputTestCase.php
index c4551e5b6..3ca7c15db 100644
--- a/Tests/Completion/Output/CompletionOutputTestCase.php
+++ b/Tests/Completion/Output/CompletionOutputTestCase.php
@@ -14,6 +14,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Output\CompletionOutputInterface;
+use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\StreamOutput;
@@ -28,8 +29,8 @@ abstract public function getExpectedValuesOutput(): string;
public function testOptionsOutput()
{
$options = [
- new InputOption('option1', 'o', InputOption::VALUE_NONE),
- new InputOption('negatable', null, InputOption::VALUE_NEGATABLE),
+ new InputOption('option1', 'o', InputOption::VALUE_NONE, 'First Option'),
+ new InputOption('negatable', null, InputOption::VALUE_NEGATABLE, 'Can be negative'),
];
$suggestions = new CompletionSuggestions();
$suggestions->suggestOptions($options);
@@ -42,7 +43,11 @@ public function testOptionsOutput()
public function testValuesOutput()
{
$suggestions = new CompletionSuggestions();
- $suggestions->suggestValues(['Green', 'Red', 'Yellow']);
+ $suggestions->suggestValues([
+ new Suggestion('Green', 'Beans are green'),
+ new Suggestion('Red', 'Rose are red'),
+ new Suggestion('Yellow', 'Canaries are yellow'),
+ ]);
$stream = fopen('php://memory', 'rw+');
$this->getCompletionOutput()->write($suggestions, new StreamOutput($stream));
fseek($stream, 0);
diff --git a/Tests/Completion/Output/FishCompletionOutputTest.php b/Tests/Completion/Output/FishCompletionOutputTest.php
new file mode 100644
index 000000000..93456e138
--- /dev/null
+++ b/Tests/Completion/Output/FishCompletionOutputTest.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Tests\Completion\Output;
+
+use Symfony\Component\Console\Completion\Output\CompletionOutputInterface;
+use Symfony\Component\Console\Completion\Output\FishCompletionOutput;
+
+class FishCompletionOutputTest extends CompletionOutputTestCase
+{
+ public function getCompletionOutput(): CompletionOutputInterface
+ {
+ return new FishCompletionOutput();
+ }
+
+ public function getExpectedOptionsOutput(): string
+ {
+ return "--option1\tFirst Option\n--negatable\tCan be negative\n--no-negatable\tCan be negative";
+ }
+
+ public function getExpectedValuesOutput(): string
+ {
+ return "Green\tBeans are green\nRed\tRose are red\nYellow\tCanaries are yellow";
+ }
+}
diff --git a/Tests/Completion/Output/ZshCompletionOutputTest.php b/Tests/Completion/Output/ZshCompletionOutputTest.php
new file mode 100644
index 000000000..74dddb4b4
--- /dev/null
+++ b/Tests/Completion/Output/ZshCompletionOutputTest.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Tests\Completion\Output;
+
+use Symfony\Component\Console\Completion\Output\CompletionOutputInterface;
+use Symfony\Component\Console\Completion\Output\ZshCompletionOutput;
+
+class ZshCompletionOutputTest extends CompletionOutputTestCase
+{
+ public function getCompletionOutput(): CompletionOutputInterface
+ {
+ return new ZshCompletionOutput();
+ }
+
+ public function getExpectedOptionsOutput(): string
+ {
+ return "--option1\tFirst Option\n--negatable\tCan be negative\n--no-negatable\tCan be negative\n";
+ }
+
+ public function getExpectedValuesOutput(): string
+ {
+ return "Green\tBeans are green\nRed\tRose are red\nYellow\tCanaries are yellow\n";
+ }
+}
diff --git a/Tests/ConsoleEventsTest.php b/Tests/ConsoleEventsTest.php
index 45eb2220d..408f8c0d3 100644
--- a/Tests/ConsoleEventsTest.php
+++ b/Tests/ConsoleEventsTest.php
@@ -13,6 +13,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
@@ -29,6 +30,19 @@
class ConsoleEventsTest extends TestCase
{
+ protected function tearDown(): void
+ {
+ if (\function_exists('pcntl_signal')) {
+ pcntl_async_signals(false);
+ // We reset all signals to their default value to avoid side effects
+ pcntl_signal(\SIGINT, \SIG_DFL);
+ pcntl_signal(\SIGTERM, \SIG_DFL);
+ pcntl_signal(\SIGUSR1, \SIG_DFL);
+ pcntl_signal(\SIGUSR2, \SIG_DFL);
+ pcntl_signal(\SIGALRM, \SIG_DFL);
+ }
+ }
+
public function testEventAliases()
{
$container = new ContainerBuilder();
@@ -58,7 +72,7 @@ public function testEventAliases()
class EventTraceSubscriber implements EventSubscriberInterface
{
- public $observedEvents = [];
+ public array $observedEvents = [];
public static function getSubscribedEvents(): array
{
@@ -75,10 +89,9 @@ public function observe(object $event): void
}
}
+#[AsCommand(name: 'fail')]
class FailingCommand extends Command
{
- protected static $defaultName = 'fail';
-
protected function execute(InputInterface $input, OutputInterface $output): int
{
throw new \RuntimeException('I failed. Sorry.');
diff --git a/Tests/CursorTest.php b/Tests/CursorTest.php
index 3c22f252d..d8ae705ea 100644
--- a/Tests/CursorTest.php
+++ b/Tests/CursorTest.php
@@ -17,6 +17,7 @@
class CursorTest extends TestCase
{
+ /** @var resource */
protected $stream;
protected function setUp(): void
@@ -26,8 +27,7 @@ protected function setUp(): void
protected function tearDown(): void
{
- fclose($this->stream);
- $this->stream = null;
+ unset($this->stream);
}
public function testMoveUpOneLine()
@@ -184,6 +184,7 @@ public function testGetCurrentPosition()
$this->assertEquals("\x1b[11;10H", $this->getOutputContent($output));
$isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes);
+ $this->assertEquals($isTtySupported, '/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT));
if ($isTtySupported) {
// When tty is supported, we can't validate the exact cursor position since it depends where the cursor is when the test runs.
diff --git a/Tests/DependencyInjection/AddConsoleCommandPassTest.php b/Tests/DependencyInjection/AddConsoleCommandPassTest.php
index 1556617fb..639e5091e 100644
--- a/Tests/DependencyInjection/AddConsoleCommandPassTest.php
+++ b/Tests/DependencyInjection/AddConsoleCommandPassTest.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Console\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\LazyCommand;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
@@ -63,7 +64,6 @@ public function testProcessRegistersLazyCommands()
$container = new ContainerBuilder();
$command = $container
->register('my-command', MyCommand::class)
- ->setPublic(false)
->addTag('console.command', ['command' => 'my:command'])
->addTag('console.command', ['command' => 'my:alias'])
;
@@ -85,7 +85,6 @@ public function testProcessFallsBackToDefaultName()
$container = new ContainerBuilder();
$container
->register('with-default-name', NamedCommand::class)
- ->setPublic(false)
->addTag('console.command')
;
@@ -103,7 +102,6 @@ public function testProcessFallsBackToDefaultName()
$container = new ContainerBuilder();
$container
->register('with-default-name', NamedCommand::class)
- ->setPublic(false)
->addTag('console.command', ['command' => 'new-name'])
;
@@ -182,8 +180,6 @@ public function testEscapesDefaultFromPhp()
public function testProcessThrowAnExceptionIfTheServiceIsAbstract()
{
- $this->expectException(\InvalidArgumentException::class);
- $this->expectExceptionMessage('The service "my-command" tagged "console.command" must not be abstract.');
$container = new ContainerBuilder();
$container->setResourceTracking(false);
$container->addCompilerPass(new AddConsoleCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
@@ -193,13 +189,14 @@ public function testProcessThrowAnExceptionIfTheServiceIsAbstract()
$definition->setAbstract(true);
$container->setDefinition('my-command', $definition);
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The service "my-command" tagged "console.command" must not be abstract.');
+
$container->compile();
}
public function testProcessThrowAnExceptionIfTheServiceIsNotASubclassOfCommand()
{
- $this->expectException(\InvalidArgumentException::class);
- $this->expectExceptionMessage('The service "my-command" tagged "console.command" must be a subclass of "Symfony\Component\Console\Command\Command".');
$container = new ContainerBuilder();
$container->setResourceTracking(false);
$container->addCompilerPass(new AddConsoleCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
@@ -208,6 +205,9 @@ public function testProcessThrowAnExceptionIfTheServiceIsNotASubclassOfCommand()
$definition->addTag('console.command');
$container->setDefinition('my-command', $definition);
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The service "my-command" tagged "console.command" must be a subclass of "Symfony\Component\Console\Command\Command".');
+
$container->compile();
}
@@ -217,10 +217,10 @@ public function testProcessPrivateServicesWithSameCommand()
$className = 'Symfony\Component\Console\Tests\DependencyInjection\MyCommand';
$definition1 = new Definition($className);
- $definition1->addTag('console.command')->setPublic(false);
+ $definition1->addTag('console.command');
$definition2 = new Definition($className);
- $definition2->addTag('console.command')->setPublic(false);
+ $definition2->addTag('console.command');
$container->setDefinition('my-command1', $definition1);
$container->setDefinition('my-command2', $definition2);
@@ -242,7 +242,7 @@ public function testProcessOnChildDefinitionWithClass()
$childId = 'my-child-command';
$parentDefinition = new Definition(/* no class */);
- $parentDefinition->setAbstract(true)->setPublic(false);
+ $parentDefinition->setAbstract(true);
$childDefinition = new ChildDefinition($parentId);
$childDefinition->addTag('console.command')->setPublic(true);
@@ -267,7 +267,7 @@ public function testProcessOnChildDefinitionWithParentClass()
$childId = 'my-child-command';
$parentDefinition = new Definition($className);
- $parentDefinition->setAbstract(true)->setPublic(false);
+ $parentDefinition->setAbstract(true);
$childDefinition = new ChildDefinition($parentId);
$childDefinition->addTag('console.command')->setPublic(true);
@@ -283,8 +283,6 @@ public function testProcessOnChildDefinitionWithParentClass()
public function testProcessOnChildDefinitionWithoutClass()
{
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('The definition for "my-child-command" has no class.');
$container = new ContainerBuilder();
$container->addCompilerPass(new AddConsoleCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
@@ -292,7 +290,7 @@ public function testProcessOnChildDefinitionWithoutClass()
$childId = 'my-child-command';
$parentDefinition = new Definition();
- $parentDefinition->setAbstract(true)->setPublic(false);
+ $parentDefinition->setAbstract(true);
$childDefinition = new ChildDefinition($parentId);
$childDefinition->addTag('console.command')->setPublic(true);
@@ -300,6 +298,9 @@ public function testProcessOnChildDefinitionWithoutClass()
$container->setDefinition($parentId, $parentDefinition);
$container->setDefinition($childId, $childDefinition);
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('The definition for "my-child-command" has no class.');
+
$container->compile();
}
}
@@ -308,23 +309,20 @@ class MyCommand extends Command
{
}
+#[AsCommand(name: 'default')]
class NamedCommand extends Command
{
- protected static $defaultName = 'default';
}
+#[AsCommand(name: '%cmd%|%cmdalias%', description: 'Creates a 80% discount')]
class EscapedDefaultsFromPhpCommand extends Command
{
- protected static $defaultName = '%cmd%|%cmdalias%';
- protected static $defaultDescription = 'Creates a 80% discount';
}
+#[AsCommand(name: '|cmdname|cmdalias', description: 'Just testing')]
class DescribedCommand extends Command
{
- public static $initCounter = 0;
-
- protected static $defaultName = '|cmdname|cmdalias';
- protected static $defaultDescription = 'Just testing';
+ public static int $initCounter = 0;
public function __construct()
{
diff --git a/Tests/Descriptor/AbstractDescriptorTestCase.php b/Tests/Descriptor/AbstractDescriptorTestCase.php
index 6e0caab54..93658f4be 100644
--- a/Tests/Descriptor/AbstractDescriptorTestCase.php
+++ b/Tests/Descriptor/AbstractDescriptorTestCase.php
@@ -48,6 +48,9 @@ public function testDescribeCommand(Command $command, $expectedDescription)
/** @dataProvider getDescribeApplicationTestData */
public function testDescribeApplication(Application $application, $expectedDescription)
{
+ // the "completion" command has dynamic help information depending on the shell
+ $application->find('completion')->setHelp('');
+
$this->assertDescription($expectedDescription, $application);
}
@@ -84,7 +87,7 @@ protected static function getDescriptionTestData(array $objects)
{
$data = [];
foreach ($objects as $name => $object) {
- $description = file_get_contents(sprintf('%s/../Fixtures/%s.%s', __DIR__, $name, static::getFormat()));
+ $description = file_get_contents(\sprintf('%s/../Fixtures/%s.%s', __DIR__, $name, static::getFormat()));
$data[] = [$object, $description];
}
diff --git a/Tests/Descriptor/ApplicationDescriptionTest.php b/Tests/Descriptor/ApplicationDescriptionTest.php
index 989521ebb..1933c985c 100644
--- a/Tests/Descriptor/ApplicationDescriptionTest.php
+++ b/Tests/Descriptor/ApplicationDescriptionTest.php
@@ -43,9 +43,6 @@ public static function getNamespacesProvider()
final class TestApplication extends Application
{
- /**
- * {@inheritdoc}
- */
protected function getDefaultCommands(): array
{
return [];
diff --git a/Tests/Descriptor/JsonDescriptorTest.php b/Tests/Descriptor/JsonDescriptorTest.php
index 648476dc4..399bd8f23 100644
--- a/Tests/Descriptor/JsonDescriptorTest.php
+++ b/Tests/Descriptor/JsonDescriptorTest.php
@@ -27,13 +27,13 @@ protected static function getFormat()
protected function normalizeOutput($output)
{
- return array_map([$this, 'normalizeOutputRecursively'], json_decode($output, true));
+ return array_map($this->normalizeOutputRecursively(...), json_decode($output, true));
}
private function normalizeOutputRecursively($output)
{
if (\is_array($output)) {
- return array_map([$this, 'normalizeOutputRecursively'], $output);
+ return array_map($this->normalizeOutputRecursively(...), $output);
}
if (null === $output) {
diff --git a/Tests/Descriptor/ReStructuredTextDescriptorTest.php b/Tests/Descriptor/ReStructuredTextDescriptorTest.php
new file mode 100644
index 000000000..137270f95
--- /dev/null
+++ b/Tests/Descriptor/ReStructuredTextDescriptorTest.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Tests\Descriptor;
+
+use Symfony\Component\Console\Descriptor\ReStructuredTextDescriptor;
+use Symfony\Component\Console\Tests\Fixtures\DescriptorApplicationMbString;
+use Symfony\Component\Console\Tests\Fixtures\DescriptorCommandMbString;
+
+class ReStructuredTextDescriptorTest extends AbstractDescriptorTestCase
+{
+ public static function getDescribeCommandTestData()
+ {
+ return self::getDescriptionTestData(array_merge(
+ ObjectsProvider::getCommands(),
+ ['command_mbstring' => new DescriptorCommandMbString()]
+ ));
+ }
+
+ public static function getDescribeApplicationTestData()
+ {
+ return self::getDescriptionTestData(array_merge(
+ ObjectsProvider::getApplications(),
+ ['application_mbstring' => new DescriptorApplicationMbString()]
+ ));
+ }
+
+ protected function getDescriptor()
+ {
+ return new ReStructuredTextDescriptor();
+ }
+
+ protected static function getFormat()
+ {
+ return 'rst';
+ }
+}
diff --git a/Tests/EventListener/ErrorListenerTest.php b/Tests/EventListener/ErrorListenerTest.php
index 2bafab040..e26109851 100644
--- a/Tests/EventListener/ErrorListenerTest.php
+++ b/Tests/EventListener/ErrorListenerTest.php
@@ -107,19 +107,6 @@ public function testAllKindsOfInputCanBeLogged()
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new StringInput('test:run --foo=bar'), 255));
}
- public function testCommandNameIsDisplayedForNonStringableInput()
- {
- $logger = $this->createMock(LoggerInterface::class);
- $logger
- ->expects($this->once())
- ->method('debug')
- ->with('Command "{command}" exited with code "{code}"', ['command' => 'test:run', 'code' => 255])
- ;
-
- $listener = new ErrorListener($logger);
- $listener->onConsoleTerminate($this->getConsoleTerminateEvent($this->createMock(InputInterface::class), 255));
- }
-
private function getConsoleTerminateEvent(InputInterface $input, $exitCode)
{
return new ConsoleTerminateEvent(new Command('test:run'), $input, $this->createMock(OutputInterface::class), $exitCode);
@@ -136,11 +123,16 @@ public function hasParameterOption($values, $onlyParams = false): bool
{
}
- public function getParameterOption($values, $default = false, $onlyParams = false)
+ public function getParameterOption($values, $default = false, $onlyParams = false): mixed
+ {
+ }
+
+ public function parse(): void
{
}
- public function parse()
+ public function __toString(): string
{
+ return '';
}
}
diff --git a/Tests/Fixtures/BarBucCommand.php b/Tests/Fixtures/BarBucCommand.php
index 52b619e82..f2a785507 100644
--- a/Tests/Fixtures/BarBucCommand.php
+++ b/Tests/Fixtures/BarBucCommand.php
@@ -4,7 +4,7 @@
class BarBucCommand extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this->setName('bar:buc');
}
diff --git a/Tests/Fixtures/BarHiddenCommand.php b/Tests/Fixtures/BarHiddenCommand.php
index 58af8d816..2d42de46e 100644
--- a/Tests/Fixtures/BarHiddenCommand.php
+++ b/Tests/Fixtures/BarHiddenCommand.php
@@ -6,12 +6,12 @@
class BarHiddenCommand extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this
->setName('bar:hidden')
->setAliases(['abarhidden'])
- ->setHidden(true)
+ ->setHidden()
;
}
diff --git a/Tests/Fixtures/DescriptorCommand1.php b/Tests/Fixtures/DescriptorCommand1.php
index 14bb20486..32d91af90 100644
--- a/Tests/Fixtures/DescriptorCommand1.php
+++ b/Tests/Fixtures/DescriptorCommand1.php
@@ -15,7 +15,7 @@
class DescriptorCommand1 extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this
->setName('descriptor:command1')
diff --git a/Tests/Fixtures/DescriptorCommand2.php b/Tests/Fixtures/DescriptorCommand2.php
index 51106b961..b9e8cb6d3 100644
--- a/Tests/Fixtures/DescriptorCommand2.php
+++ b/Tests/Fixtures/DescriptorCommand2.php
@@ -17,7 +17,7 @@
class DescriptorCommand2 extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this
->setName('descriptor:command2')
diff --git a/Tests/Fixtures/DescriptorCommand3.php b/Tests/Fixtures/DescriptorCommand3.php
index 77f92e233..14fa7aee8 100644
--- a/Tests/Fixtures/DescriptorCommand3.php
+++ b/Tests/Fixtures/DescriptorCommand3.php
@@ -15,13 +15,13 @@
class DescriptorCommand3 extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this
->setName('descriptor:command3')
->setDescription('command 3 description')
->setHelp('command 3 help')
- ->setHidden(true)
+ ->setHidden()
;
}
}
diff --git a/Tests/Fixtures/DescriptorCommand4.php b/Tests/Fixtures/DescriptorCommand4.php
index 22dcae0e3..4e677feeb 100644
--- a/Tests/Fixtures/DescriptorCommand4.php
+++ b/Tests/Fixtures/DescriptorCommand4.php
@@ -15,7 +15,7 @@
class DescriptorCommand4 extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this
->setName('descriptor:command4')
diff --git a/Tests/Fixtures/DescriptorCommandMbString.php b/Tests/Fixtures/DescriptorCommandMbString.php
index 66de917e2..ba6be60e2 100644
--- a/Tests/Fixtures/DescriptorCommandMbString.php
+++ b/Tests/Fixtures/DescriptorCommandMbString.php
@@ -17,7 +17,7 @@
class DescriptorCommandMbString extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this
->setName('descriptor:åèä')
diff --git a/Tests/Fixtures/Foo1Command.php b/Tests/Fixtures/Foo1Command.php
index c4fc0e811..f8a80c54a 100644
--- a/Tests/Fixtures/Foo1Command.php
+++ b/Tests/Fixtures/Foo1Command.php
@@ -9,7 +9,7 @@ class Foo1Command extends Command
public $input;
public $output;
- protected function configure()
+ protected function configure(): void
{
$this
->setName('foo:bar1')
diff --git a/Tests/Fixtures/Foo2Command.php b/Tests/Fixtures/Foo2Command.php
index b3b6e6c06..39df932b9 100644
--- a/Tests/Fixtures/Foo2Command.php
+++ b/Tests/Fixtures/Foo2Command.php
@@ -6,7 +6,7 @@
class Foo2Command extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this
->setName('foo1:bar')
diff --git a/Tests/Fixtures/Foo3Command.php b/Tests/Fixtures/Foo3Command.php
index d6ca5cd15..505b2d58e 100644
--- a/Tests/Fixtures/Foo3Command.php
+++ b/Tests/Fixtures/Foo3Command.php
@@ -6,7 +6,7 @@
class Foo3Command extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this
->setName('foo3:bar')
diff --git a/Tests/Fixtures/Foo4Command.php b/Tests/Fixtures/Foo4Command.php
index 1c5463995..3f826a83a 100644
--- a/Tests/Fixtures/Foo4Command.php
+++ b/Tests/Fixtures/Foo4Command.php
@@ -4,7 +4,7 @@
class Foo4Command extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this->setName('foo3:bar:toh');
}
diff --git a/Tests/Fixtures/Foo6Command.php b/Tests/Fixtures/Foo6Command.php
index ef5bd7702..b71022ae5 100644
--- a/Tests/Fixtures/Foo6Command.php
+++ b/Tests/Fixtures/Foo6Command.php
@@ -4,7 +4,7 @@
class Foo6Command extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this->setName('0foo:bar')->setDescription('0foo:bar command');
}
diff --git a/Tests/Fixtures/FooCommand.php b/Tests/Fixtures/FooCommand.php
index fac25f21e..d8c39ae49 100644
--- a/Tests/Fixtures/FooCommand.php
+++ b/Tests/Fixtures/FooCommand.php
@@ -6,10 +6,10 @@
class FooCommand extends Command
{
- public $input;
- public $output;
+ public InputInterface $input;
+ public OutputInterface $output;
- protected function configure()
+ protected function configure(): void
{
$this
->setName('foo:bar')
@@ -18,7 +18,7 @@ protected function configure()
;
}
- protected function interact(InputInterface $input, OutputInterface $output)
+ protected function interact(InputInterface $input, OutputInterface $output): void
{
$output->writeln('interact called');
}
diff --git a/Tests/Fixtures/FooHiddenCommand.php b/Tests/Fixtures/FooHiddenCommand.php
index 68e495b76..b9ef2576b 100644
--- a/Tests/Fixtures/FooHiddenCommand.php
+++ b/Tests/Fixtures/FooHiddenCommand.php
@@ -6,12 +6,12 @@
class FooHiddenCommand extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this
->setName('foo:hidden')
->setAliases(['afoohidden'])
- ->setHidden(true)
+ ->setHidden()
;
}
diff --git a/Tests/Fixtures/FooLock2Command.php b/Tests/Fixtures/FooLock2Command.php
index dcf78bee7..d7521c4ee 100644
--- a/Tests/Fixtures/FooLock2Command.php
+++ b/Tests/Fixtures/FooLock2Command.php
@@ -9,7 +9,7 @@ class FooLock2Command extends Command
{
use LockableTrait;
- protected function configure()
+ protected function configure(): void
{
$this->setName('foo:lock2');
}
diff --git a/Tests/Fixtures/FooLock3Command.php b/Tests/Fixtures/FooLock3Command.php
new file mode 100644
index 000000000..78492de69
--- /dev/null
+++ b/Tests/Fixtures/FooLock3Command.php
@@ -0,0 +1,35 @@
+lockFactory = $lockFactory;
+ }
+
+ protected function configure(): void
+ {
+ $this->setName('foo:lock3');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ if (!$this->lock()) {
+ return 1;
+ }
+
+ $this->release();
+
+ return 2;
+ }
+}
diff --git a/Tests/Fixtures/FooLockCommand.php b/Tests/Fixtures/FooLockCommand.php
index 103954f4f..df33c171c 100644
--- a/Tests/Fixtures/FooLockCommand.php
+++ b/Tests/Fixtures/FooLockCommand.php
@@ -9,7 +9,7 @@ class FooLockCommand extends Command
{
use LockableTrait;
- protected function configure()
+ protected function configure(): void
{
$this->setName('foo:lock');
}
diff --git a/Tests/Fixtures/FooOptCommand.php b/Tests/Fixtures/FooOptCommand.php
index dea6d4d16..e9147352c 100644
--- a/Tests/Fixtures/FooOptCommand.php
+++ b/Tests/Fixtures/FooOptCommand.php
@@ -10,7 +10,7 @@ class FooOptCommand extends Command
public $input;
public $output;
- protected function configure()
+ protected function configure(): void
{
$this
->setName('foo:bar')
@@ -20,7 +20,7 @@ protected function configure()
;
}
- protected function interact(InputInterface $input, OutputInterface $output)
+ protected function interact(InputInterface $input, OutputInterface $output): void
{
$output->writeln('interact called');
}
diff --git a/Tests/Fixtures/FooSameCaseLowercaseCommand.php b/Tests/Fixtures/FooSameCaseLowercaseCommand.php
index c875be0cd..7f0e2c2e6 100644
--- a/Tests/Fixtures/FooSameCaseLowercaseCommand.php
+++ b/Tests/Fixtures/FooSameCaseLowercaseCommand.php
@@ -4,7 +4,7 @@
class FooSameCaseLowercaseCommand extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this->setName('foo:bar')->setDescription('foo:bar command');
}
diff --git a/Tests/Fixtures/FooSameCaseUppercaseCommand.php b/Tests/Fixtures/FooSameCaseUppercaseCommand.php
index 75c8d0024..570726897 100644
--- a/Tests/Fixtures/FooSameCaseUppercaseCommand.php
+++ b/Tests/Fixtures/FooSameCaseUppercaseCommand.php
@@ -4,7 +4,7 @@
class FooSameCaseUppercaseCommand extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this->setName('foo:BAR')->setDescription('foo:BAR command');
}
diff --git a/Tests/Fixtures/FooSubnamespaced1Command.php b/Tests/Fixtures/FooSubnamespaced1Command.php
index c9fad78e1..72b389a67 100644
--- a/Tests/Fixtures/FooSubnamespaced1Command.php
+++ b/Tests/Fixtures/FooSubnamespaced1Command.php
@@ -6,10 +6,10 @@
class FooSubnamespaced1Command extends Command
{
- public $input;
- public $output;
+ public InputInterface $input;
+ public OutputInterface $output;
- protected function configure()
+ protected function configure(): void
{
$this
->setName('foo:bar:baz')
diff --git a/Tests/Fixtures/FooSubnamespaced2Command.php b/Tests/Fixtures/FooSubnamespaced2Command.php
index dea20a49c..9867a07a5 100644
--- a/Tests/Fixtures/FooSubnamespaced2Command.php
+++ b/Tests/Fixtures/FooSubnamespaced2Command.php
@@ -6,10 +6,10 @@
class FooSubnamespaced2Command extends Command
{
- public $input;
- public $output;
+ public InputInterface $input;
+ public OutputInterface $output;
- protected function configure()
+ protected function configure(): void
{
$this
->setName('foo:go:bret')
diff --git a/Tests/Fixtures/FooWithoutAliasCommand.php b/Tests/Fixtures/FooWithoutAliasCommand.php
index 3d125500c..268d0b390 100644
--- a/Tests/Fixtures/FooWithoutAliasCommand.php
+++ b/Tests/Fixtures/FooWithoutAliasCommand.php
@@ -6,7 +6,7 @@
class FooWithoutAliasCommand extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this
->setName('foo')
diff --git a/Tests/Fixtures/FoobarCommand.php b/Tests/Fixtures/FoobarCommand.php
index ab3c9ff0c..9405b0917 100644
--- a/Tests/Fixtures/FoobarCommand.php
+++ b/Tests/Fixtures/FoobarCommand.php
@@ -9,7 +9,7 @@ class FoobarCommand extends Command
public $input;
public $output;
- protected function configure()
+ protected function configure(): void
{
$this
->setName('foobar:foo')
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_13.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_13.php
index 827cbad1d..9bcc68f69 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_13.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_13.php
@@ -9,6 +9,6 @@
$output->setDecorated(true);
$output = new SymfonyStyle($input, $output);
$output->comment(
- 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum'
+ 'Árvíztűrőtükörfúrógép 🎼 Lorem ipsum dolor sit 💕 amet, consectetur adipisicing elit, sed do eiusmod tempor incididu labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum'
);
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_19.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_19.php
index e44b18b76..e25a7ef29 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_19.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_19.php
@@ -1,6 +1,5 @@
setDecorated(true);
+ $output = new SymfonyStyle($input, $output);
+ $output->block(
+ 'Árvíztűrőtükörfúrógép Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum',
+ '★', // UTF-8 star!
+ null,
+ ' ║ >', // UTF-8 double line!
+ false,
+ false
+ );
+};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_23.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_23.php
new file mode 100644
index 000000000..e6228fe0b
--- /dev/null
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_23.php
@@ -0,0 +1,10 @@
+text('Hello');
+};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt b/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt
index ea8e4351e..d5caad86f 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt
+++ b/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt
@@ -1,7 +1,7 @@
-[39;49m // [39;49mLorem ipsum dolor sit [33mamet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore [39m
-[39;49m // [39;49m[33mmagna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo [39m
-[39;49m // [39;49m[33mconsequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla [39m
-[39;49m // [39;49m[33mpariatur.[39m Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
-[39;49m // [39;49mest laborum
+[39;49m // [39;49mÁrvíztűrőtükörfúrógép 🎼 Lorem ipsum dolor sit [33m💕 amet, consectetur adipisicing elit, sed do eiusmod tempor incididu[39m
+[39;49m // [39;49m[33mlabore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex[39m
+[39;49m // [39;49m[33mea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla [39m
+[39;49m // [39;49m[33mpariatur.[39m Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est
+[39;49m // [39;49mlaborum
diff --git a/Tests/Fixtures/Style/SymfonyStyle/output/output_22.txt b/Tests/Fixtures/Style/SymfonyStyle/output/output_22.txt
new file mode 100644
index 000000000..e9f06b1c9
--- /dev/null
+++ b/Tests/Fixtures/Style/SymfonyStyle/output/output_22.txt
@@ -0,0 +1,7 @@
+
+[39;49m ║ [39;49m[★] Árvíztűrőtükörfúrógép Lorem ipsum dolor sit [33mamet, consectetur adipisicing elit, sed do eiusmod tempor incididunt [39m
+[39;49m ║ [39;49m[33m ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut [39m
+[39;49m ║ [39;49m[33m aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu [39m
+[39;49m ║ [39;49m[33m fugiat nulla pariatur.[39m Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit
+[39;49m ║ [39;49m anim id est laborum
+
diff --git a/Tests/Fixtures/Style/SymfonyStyle/output/output_23.txt b/Tests/Fixtures/Style/SymfonyStyle/output/output_23.txt
new file mode 100644
index 000000000..63105f17b
--- /dev/null
+++ b/Tests/Fixtures/Style/SymfonyStyle/output/output_23.txt
@@ -0,0 +1 @@
+ Hello
diff --git a/Tests/Fixtures/Style/SymfonyStyle/output/output_6.txt b/Tests/Fixtures/Style/SymfonyStyle/output/output_6.txt
index 5f2d33c14..7f1d478a3 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/output/output_6.txt
+++ b/Tests/Fixtures/Style/SymfonyStyle/output/output_6.txt
@@ -1,4 +1,3 @@
-
* Lorem ipsum dolor sit amet
* consectetur adipiscing elit
diff --git a/Tests/Fixtures/TestAmbiguousCommandRegistering.php b/Tests/Fixtures/TestAmbiguousCommandRegistering.php
index 71badf342..cc3967c06 100644
--- a/Tests/Fixtures/TestAmbiguousCommandRegistering.php
+++ b/Tests/Fixtures/TestAmbiguousCommandRegistering.php
@@ -6,7 +6,7 @@
class TestAmbiguousCommandRegistering extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this
->setName('test-ambiguous')
diff --git a/Tests/Fixtures/TestAmbiguousCommandRegistering2.php b/Tests/Fixtures/TestAmbiguousCommandRegistering2.php
index 127ea56a9..3b8dcb2d2 100644
--- a/Tests/Fixtures/TestAmbiguousCommandRegistering2.php
+++ b/Tests/Fixtures/TestAmbiguousCommandRegistering2.php
@@ -6,7 +6,7 @@
class TestAmbiguousCommandRegistering2 extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this
->setName('test-ambiguous2')
diff --git a/Tests/Fixtures/TestCommand.php b/Tests/Fixtures/TestCommand.php
index 7de01cebe..1d2b45af1 100644
--- a/Tests/Fixtures/TestCommand.php
+++ b/Tests/Fixtures/TestCommand.php
@@ -6,7 +6,7 @@
class TestCommand extends Command
{
- protected function configure()
+ protected function configure(): void
{
$this
->setName('namespace:name')
@@ -23,7 +23,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return 0;
}
- protected function interact(InputInterface $input, OutputInterface $output)
+ protected function interact(InputInterface $input, OutputInterface $output): void
{
$output->writeln('interact called');
}
diff --git a/Tests/Fixtures/application_1.json b/Tests/Fixtures/application_1.json
index f7e82fb12..1477659ad 100644
--- a/Tests/Fixtures/application_1.json
+++ b/Tests/Fixtures/application_1.json
@@ -4,7 +4,7 @@
"name": "_complete",
"hidden": true,
"usage": [
- "_complete [-s|--shell SHELL] [-i|--input INPUT] [-c|--current CURRENT] [-S|--symfony SYMFONY]"
+ "_complete [-s|--shell SHELL] [-i|--input INPUT] [-c|--current CURRENT] [-a|--api-version API-VERSION] [-S|--symfony SYMFONY]"
],
"description": "Internal command to provide shell completion suggestions",
"help": "Internal command to provide shell completion suggestions",
@@ -17,7 +17,7 @@
"accept_value": true,
"is_value_required": true,
"is_multiple": false,
- "description": "The version of the completion script",
+ "description": "deprecated",
"default": null
},
"help": {
@@ -29,13 +29,22 @@
"description": "Display help for the given command. When no command is given display help for the list command",
"default": false
},
+ "silent": {
+ "name": "--silent",
+ "shortcut": "",
+ "accept_value": false,
+ "is_value_required": false,
+ "is_multiple": false,
+ "description": "Do not output any message",
+ "default": false
+ },
"quiet": {
"name": "--quiet",
"shortcut": "-q",
"accept_value": false,
"is_value_required": false,
"is_multiple": false,
- "description": "Do not output any message",
+ "description": "Only errors are displayed. All other output is suppressed",
"default": false
},
"verbose": {
@@ -89,7 +98,7 @@
"accept_value": true,
"is_value_required": true,
"is_multiple": false,
- "description": "The shell type (\"bash\")",
+ "description": "The shell type (\"bash\", \"fish\", \"zsh\")",
"default": null
},
"current": {
@@ -109,6 +118,15 @@
"is_multiple": true,
"description": "An array of input tokens (e.g. COMP_WORDS or argv)",
"default": []
+ },
+ "api-version": {
+ "name": "--api-version",
+ "shortcut": "-a",
+ "accept_value": true,
+ "is_value_required": true,
+ "is_multiple": false,
+ "description": "The API version of the completion script",
+ "default": null
}
}
}
@@ -120,7 +138,7 @@
"completion [--debug] [--] []"
],
"description": "Dump the shell completion script",
- "help": "The completion> command dumps the shell completion script required\nto use shell autocompletion (currently only bash completion is supported).\n\nStatic installation\n------------------->\n\nDump the script to a global completion file and restart your shell:\n\n %%PHP_SELF%% completion bash | sudo tee /etc/bash_completion.d/%%COMMAND_NAME%%>\n\nOr dump the script to a local file and source it:\n\n %%PHP_SELF%% completion bash > completion.sh>\n\n # source the file whenever you use the project>\n source completion.sh>\n\n # or add this line at the end of your \"~/.bashrc\" file:>\n source /path/to/completion.sh>\n\nDynamic installation\n-------------------->\n\nAdd this to the end of your shell configuration file (e.g. \"~/.bashrc\">):\n\n eval \"$(%%PHP_SELF_FULL%% completion bash)\">",
+ "help": "Dump the shell completion script",
"definition": {
"arguments": {
"shell": {
@@ -141,13 +159,22 @@
"description": "Display help for the given command. When no command is given display help for the list command",
"default": false
},
+ "silent": {
+ "name": "--silent",
+ "shortcut": "",
+ "accept_value": false,
+ "is_value_required": false,
+ "is_multiple": false,
+ "description": "Do not output any message",
+ "default": false
+ },
"quiet": {
"name": "--quiet",
"shortcut": "-q",
"accept_value": false,
"is_value_required": false,
"is_multiple": false,
- "description": "Do not output any message",
+ "description": "Only errors are displayed. All other output is suppressed",
"default": false
},
"verbose": {
@@ -253,13 +280,22 @@
"description": "Display help for the given command. When no command is given display help for the list command",
"default": false
},
+ "silent": {
+ "name": "--silent",
+ "shortcut": "",
+ "accept_value": false,
+ "is_value_required": false,
+ "is_multiple": false,
+ "description": "Do not output any message",
+ "default": false
+ },
"quiet": {
"name": "--quiet",
"shortcut": "-q",
"accept_value": false,
"is_value_required": false,
"is_multiple": false,
- "description": "Do not output any message",
+ "description": "Only errors are displayed. All other output is suppressed",
"default": false
},
"verbose": {
@@ -356,13 +392,22 @@
"description": "Display help for the given command. When no command is given display help for the list command",
"default": false
},
+ "silent": {
+ "name": "--silent",
+ "shortcut": "",
+ "accept_value": false,
+ "is_value_required": false,
+ "is_multiple": false,
+ "description": "Do not output any message",
+ "default": false
+ },
"quiet": {
"name": "--quiet",
"shortcut": "-q",
"accept_value": false,
"is_value_required": false,
"is_multiple": false,
- "description": "Do not output any message",
+ "description": "Only errors are displayed. All other output is suppressed",
"default": false
},
"verbose": {
diff --git a/Tests/Fixtures/application_1.md b/Tests/Fixtures/application_1.md
index 0bef4ce31..79d9b27aa 100644
--- a/Tests/Fixtures/application_1.md
+++ b/Tests/Fixtures/application_1.md
@@ -14,32 +14,7 @@ Dump the shell completion script
* `completion [--debug] [--] []`
-The completion command dumps the shell completion script required
-to use shell autocompletion (currently only bash completion is supported).
-
-Static installation
--------------------
-
-Dump the script to a global completion file and restart your shell:
-
- %%PHP_SELF%% completion bash | sudo tee /etc/bash_completion.d/%%COMMAND_NAME%%
-
-Or dump the script to a local file and source it:
-
- %%PHP_SELF%% completion bash > completion.sh
-
- # source the file whenever you use the project
- source completion.sh
-
- # or add this line at the end of your "~/.bashrc" file:
- source /path/to/completion.sh
-
-Dynamic installation
---------------------
-
-Add this to the end of your shell configuration file (e.g. "~/.bashrc"):
-
- eval "$(%%PHP_SELF_FULL%% completion bash)"
+Dump the shell completion script
### Arguments
@@ -73,7 +48,7 @@ Display help for the given command. When no command is given display help for th
* Is negatable: no
* Default: `false`
-#### `--quiet|-q`
+#### `--silent`
Do not output any message
@@ -83,6 +58,16 @@ Do not output any message
* Is negatable: no
* Default: `false`
+#### `--quiet|-q`
+
+Only errors are displayed. All other output is suppressed
+
+* Accept value: no
+* Is value required: no
+* Is multiple: no
+* Is negatable: no
+* Default: `false`
+
#### `--verbose|-v|-vv|-vvv`
Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
@@ -184,7 +169,7 @@ Display help for the given command. When no command is given display help for th
* Is negatable: no
* Default: `false`
-#### `--quiet|-q`
+#### `--silent`
Do not output any message
@@ -194,6 +179,16 @@ Do not output any message
* Is negatable: no
* Default: `false`
+#### `--quiet|-q`
+
+Only errors are displayed. All other output is suppressed
+
+* Accept value: no
+* Is value required: no
+* Is multiple: no
+* Is negatable: no
+* Default: `false`
+
#### `--verbose|-v|-vv|-vvv`
Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
@@ -311,7 +306,7 @@ Display help for the given command. When no command is given display help for th
* Is negatable: no
* Default: `false`
-#### `--quiet|-q`
+#### `--silent`
Do not output any message
@@ -321,6 +316,16 @@ Do not output any message
* Is negatable: no
* Default: `false`
+#### `--quiet|-q`
+
+Only errors are displayed. All other output is suppressed
+
+* Accept value: no
+* Is value required: no
+* Is multiple: no
+* Is negatable: no
+* Default: `false`
+
#### `--verbose|-v|-vv|-vvv`
Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
diff --git a/Tests/Fixtures/application_1.rst b/Tests/Fixtures/application_1.rst
new file mode 100644
index 000000000..5da38d0ff
--- /dev/null
+++ b/Tests/Fixtures/application_1.rst
@@ -0,0 +1,135 @@
+Console Tool
+============
+
+Table of Contents
+-----------------
+
+
+
+- `help`_
+- `list`_
+
+Commands
+--------
+
+Global
+~~~~~~
+
+help
+....
+
+Display help for a command
+
+Usage
+^^^^^
+
+- ``help [--format FORMAT] [--raw] [--] []``
+
+The help command displays help for a given command:
+
+ %%PHP_SELF%% help list
+
+You can also output the help in other formats by using the --format option:
+
+ %%PHP_SELF%% help --format=xml list
+
+To display the list of available commands, please use the list command.
+
+Arguments
+^^^^^^^^^
+
+command_name
+
+Options
+^^^^^^^
+
+\-\-format
+""""""""""
+
+The output format (txt, xml, json, or md)
+
+- **Accept value**: yes
+- **Is value required**: yes
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``'txt'``
+
+\-\-raw
+"""""""
+
+To output raw command help
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
+
+
+
+list
+....
+
+List commands
+
+Usage
+^^^^^
+
+- ``list [--raw] [--format FORMAT] [--short] [--] []``
+
+The list command lists all commands:
+
+ %%PHP_SELF%% list
+
+You can also display the commands for a specific namespace:
+
+ %%PHP_SELF%% list test
+
+You can also output the information in other formats by using the --format option:
+
+ %%PHP_SELF%% list --format=xml
+
+It's also possible to get raw list of commands (useful for embedding command runner):
+
+ %%PHP_SELF%% list --raw
+
+Arguments
+^^^^^^^^^
+
+namespace
+
+Options
+^^^^^^^
+
+\-\-raw
+"""""""
+
+To output raw command list
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
+
+\-\-format
+""""""""""
+
+The output format (txt, xml, json, or md)
+
+- **Accept value**: yes
+- **Is value required**: yes
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``'txt'``
+
+\-\-short
+"""""""""
+
+To skip describing commands' arguments
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
diff --git a/Tests/Fixtures/application_1.txt b/Tests/Fixtures/application_1.txt
index f72f43a07..7fce7ce52 100644
--- a/Tests/Fixtures/application_1.txt
+++ b/Tests/Fixtures/application_1.txt
@@ -5,7 +5,8 @@ Console Tool
Options:
-h, --help Display help for the given command. When no command is given display help for the list command
- -q, --quiet Do not output any message
+ --silent Do not output any message
+ -q, --quiet Only errors are displayed. All other output is suppressed
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
diff --git a/Tests/Fixtures/application_1.xml b/Tests/Fixtures/application_1.xml
index c35d8bac9..d726cee35 100644
--- a/Tests/Fixtures/application_1.xml
+++ b/Tests/Fixtures/application_1.xml
@@ -3,14 +3,14 @@
- _complete [-s|--shell SHELL] [-i|--input INPUT] [-c|--current CURRENT] [-S|--symfony SYMFONY]
+ _complete [-s|--shell SHELL] [-i|--input INPUT] [-c|--current CURRENT] [-a|--api-version API-VERSION] [-S|--symfony SYMFONY]
Internal command to provide shell completion suggestions
Internal command to provide shell completion suggestions
+
-
+
@@ -53,32 +60,7 @@
completion [--debug] [--] [<shell>]
Dump the shell completion script
- The <info>completion</> command dumps the shell completion script required
- to use shell autocompletion (currently only bash completion is supported).
-
- <comment>Static installation
- -------------------</>
-
- Dump the script to a global completion file and restart your shell:
-
- <info>%%PHP_SELF%% completion bash | sudo tee /etc/bash_completion.d/%%COMMAND_NAME%%</>
-
- Or dump the script to a local file and source it:
-
- <info>%%PHP_SELF%% completion bash > completion.sh</>
-
- <comment># source the file whenever you use the project</>
- <info>source completion.sh</>
-
- <comment># or add this line at the end of your "~/.bashrc" file:</>
- <info>source /path/to/completion.sh</>
-
- <comment>Dynamic installation
- --------------------</>
-
- Add this to the end of your shell configuration file (e.g. <info>"~/.bashrc"</>):
-
- <info>eval "$(%%PHP_SELF_FULL%% completion bash)"</>
+ Dump the shell completion script
The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given
@@ -92,9 +74,12 @@
-
+
@@ -147,9 +132,12 @@
-
+
@@ -209,9 +197,12 @@
-
+
diff --git a/Tests/Fixtures/application_2.json b/Tests/Fixtures/application_2.json
index c5b728c6d..4a6f411f5 100644
--- a/Tests/Fixtures/application_2.json
+++ b/Tests/Fixtures/application_2.json
@@ -8,7 +8,7 @@
"name": "_complete",
"hidden": true,
"usage": [
- "_complete [-s|--shell SHELL] [-i|--input INPUT] [-c|--current CURRENT] [-S|--symfony SYMFONY]"
+ "_complete [-s|--shell SHELL] [-i|--input INPUT] [-c|--current CURRENT] [-a|--api-version API-VERSION] [-S|--symfony SYMFONY]"
],
"description": "Internal command to provide shell completion suggestions",
"help": "Internal command to provide shell completion suggestions",
@@ -21,7 +21,7 @@
"accept_value": true,
"is_value_required": true,
"is_multiple": false,
- "description": "The version of the completion script",
+ "description": "deprecated",
"default": null
},
"help": {
@@ -33,13 +33,22 @@
"description": "Display help for the given command. When no command is given display help for the list command",
"default": false
},
+ "silent": {
+ "name": "--silent",
+ "shortcut": "",
+ "accept_value": false,
+ "is_value_required": false,
+ "is_multiple": false,
+ "description": "Do not output any message",
+ "default": false
+ },
"quiet": {
"name": "--quiet",
"shortcut": "-q",
"accept_value": false,
"is_value_required": false,
"is_multiple": false,
- "description": "Do not output any message",
+ "description": "Only errors are displayed. All other output is suppressed",
"default": false
},
"verbose": {
@@ -93,7 +102,7 @@
"accept_value": true,
"is_value_required": true,
"is_multiple": false,
- "description": "The shell type (\"bash\")",
+ "description": "The shell type (\"bash\", \"fish\", \"zsh\")",
"default": null
},
"current": {
@@ -113,6 +122,15 @@
"is_multiple": true,
"description": "An array of input tokens (e.g. COMP_WORDS or argv)",
"default": []
+ },
+ "api-version": {
+ "name": "--api-version",
+ "shortcut": "-a",
+ "accept_value": true,
+ "is_value_required": true,
+ "is_multiple": false,
+ "description": "The API version of the completion script",
+ "default": null
}
}
}
@@ -124,7 +142,7 @@
"completion [--debug] [--] []"
],
"description": "Dump the shell completion script",
- "help": "The completion> command dumps the shell completion script required\nto use shell autocompletion (currently only bash completion is supported).\n\nStatic installation\n------------------->\n\nDump the script to a global completion file and restart your shell:\n\n %%PHP_SELF%% completion bash | sudo tee /etc/bash_completion.d/%%COMMAND_NAME%%>\n\nOr dump the script to a local file and source it:\n\n %%PHP_SELF%% completion bash > completion.sh>\n\n # source the file whenever you use the project>\n source completion.sh>\n\n # or add this line at the end of your \"~/.bashrc\" file:>\n source /path/to/completion.sh>\n\nDynamic installation\n-------------------->\n\nAdd this to the end of your shell configuration file (e.g. \"~/.bashrc\">):\n\n eval \"$(%%PHP_SELF_FULL%% completion bash)\">",
+ "help": "Dump the shell completion script",
"definition": {
"arguments": {
"shell": {
@@ -145,13 +163,22 @@
"description": "Display help for the given command. When no command is given display help for the list command",
"default": false
},
+ "silent": {
+ "name": "--silent",
+ "shortcut": "",
+ "accept_value": false,
+ "is_value_required": false,
+ "is_multiple": false,
+ "description": "Do not output any message",
+ "default": false
+ },
"quiet": {
"name": "--quiet",
"shortcut": "-q",
"accept_value": false,
"is_value_required": false,
"is_multiple": false,
- "description": "Do not output any message",
+ "description": "Only errors are displayed. All other output is suppressed",
"default": false
},
"verbose": {
@@ -257,13 +284,22 @@
"description": "Display help for the given command. When no command is given display help for the list command",
"default": false
},
+ "silent": {
+ "name": "--silent",
+ "shortcut": "",
+ "accept_value": false,
+ "is_value_required": false,
+ "is_multiple": false,
+ "description": "Do not output any message",
+ "default": false
+ },
"quiet": {
"name": "--quiet",
"shortcut": "-q",
"accept_value": false,
"is_value_required": false,
"is_multiple": false,
- "description": "Do not output any message",
+ "description": "Only errors are displayed. All other output is suppressed",
"default": false
},
"verbose": {
@@ -360,13 +396,22 @@
"description": "Display help for the given command. When no command is given display help for the list command",
"default": false
},
+ "silent": {
+ "name": "--silent",
+ "shortcut": "",
+ "accept_value": false,
+ "is_value_required": false,
+ "is_multiple": false,
+ "description": "Do not output any message",
+ "default": false
+ },
"quiet": {
"name": "--quiet",
"shortcut": "-q",
"accept_value": false,
"is_value_required": false,
"is_multiple": false,
- "description": "Do not output any message",
+ "description": "Only errors are displayed. All other output is suppressed",
"default": false
},
"verbose": {
@@ -448,13 +493,22 @@
"description": "Display help for the given command. When no command is given display help for the list command",
"default": false
},
+ "silent": {
+ "name": "--silent",
+ "shortcut": "",
+ "accept_value": false,
+ "is_value_required": false,
+ "is_multiple": false,
+ "description": "Do not output any message",
+ "default": false
+ },
"quiet": {
"name": "--quiet",
"shortcut": "-q",
"accept_value": false,
"is_value_required": false,
"is_multiple": false,
- "description": "Do not output any message",
+ "description": "Only errors are displayed. All other output is suppressed",
"default": false
},
"verbose": {
@@ -544,13 +598,22 @@
"description": "Display help for the given command. When no command is given display help for the list command",
"default": false
},
+ "silent": {
+ "name": "--silent",
+ "shortcut": "",
+ "accept_value": false,
+ "is_value_required": false,
+ "is_multiple": false,
+ "description": "Do not output any message",
+ "default": false
+ },
"quiet": {
"name": "--quiet",
"shortcut": "-q",
"accept_value": false,
"is_value_required": false,
"is_multiple": false,
- "description": "Do not output any message",
+ "description": "Only errors are displayed. All other output is suppressed",
"default": false
},
"verbose": {
@@ -621,13 +684,22 @@
"description": "Display help for the given command. When no command is given display help for the list command",
"default": false
},
+ "silent": {
+ "name": "--silent",
+ "shortcut": "",
+ "accept_value": false,
+ "is_value_required": false,
+ "is_multiple": false,
+ "description": "Do not output any message",
+ "default": false
+ },
"quiet": {
"name": "--quiet",
"shortcut": "-q",
"accept_value": false,
"is_value_required": false,
"is_multiple": false,
- "description": "Do not output any message",
+ "description": "Only errors are displayed. All other output is suppressed",
"default": false
},
"verbose": {
@@ -700,13 +772,22 @@
"description": "Display help for the given command. When no command is given display help for the list command",
"default": false
},
+ "silent": {
+ "name": "--silent",
+ "shortcut": "",
+ "accept_value": false,
+ "is_value_required": false,
+ "is_multiple": false,
+ "description": "Do not output any message",
+ "default": false
+ },
"quiet": {
"name": "--quiet",
"shortcut": "-q",
"accept_value": false,
"is_value_required": false,
"is_multiple": false,
- "description": "Do not output any message",
+ "description": "Only errors are displayed. All other output is suppressed",
"default": false
},
"verbose": {
diff --git a/Tests/Fixtures/application_2.md b/Tests/Fixtures/application_2.md
index 2fa9a2208..37e6c28fc 100644
--- a/Tests/Fixtures/application_2.md
+++ b/Tests/Fixtures/application_2.md
@@ -27,32 +27,7 @@ Dump the shell completion script
* `completion [--debug] [--] []`
-The completion command dumps the shell completion script required
-to use shell autocompletion (currently only bash completion is supported).
-
-Static installation
--------------------
-
-Dump the script to a global completion file and restart your shell:
-
- %%PHP_SELF%% completion bash | sudo tee /etc/bash_completion.d/%%COMMAND_NAME%%
-
-Or dump the script to a local file and source it:
-
- %%PHP_SELF%% completion bash > completion.sh
-
- # source the file whenever you use the project
- source completion.sh
-
- # or add this line at the end of your "~/.bashrc" file:
- source /path/to/completion.sh
-
-Dynamic installation
---------------------
-
-Add this to the end of your shell configuration file (e.g. "~/.bashrc"):
-
- eval "$(%%PHP_SELF_FULL%% completion bash)"
+Dump the shell completion script
### Arguments
@@ -86,7 +61,7 @@ Display help for the given command. When no command is given display help for th
* Is negatable: no
* Default: `false`
-#### `--quiet|-q`
+#### `--silent`
Do not output any message
@@ -96,6 +71,16 @@ Do not output any message
* Is negatable: no
* Default: `false`
+#### `--quiet|-q`
+
+Only errors are displayed. All other output is suppressed
+
+* Accept value: no
+* Is value required: no
+* Is multiple: no
+* Is negatable: no
+* Default: `false`
+
#### `--verbose|-v|-vv|-vvv`
Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
@@ -197,7 +182,7 @@ Display help for the given command. When no command is given display help for th
* Is negatable: no
* Default: `false`
-#### `--quiet|-q`
+#### `--silent`
Do not output any message
@@ -207,6 +192,16 @@ Do not output any message
* Is negatable: no
* Default: `false`
+#### `--quiet|-q`
+
+Only errors are displayed. All other output is suppressed
+
+* Accept value: no
+* Is value required: no
+* Is multiple: no
+* Is negatable: no
+* Default: `false`
+
#### `--verbose|-v|-vv|-vvv`
Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
@@ -324,7 +319,7 @@ Display help for the given command. When no command is given display help for th
* Is negatable: no
* Default: `false`
-#### `--quiet|-q`
+#### `--silent`
Do not output any message
@@ -334,6 +329,16 @@ Do not output any message
* Is negatable: no
* Default: `false`
+#### `--quiet|-q`
+
+Only errors are displayed. All other output is suppressed
+
+* Accept value: no
+* Is value required: no
+* Is multiple: no
+* Is negatable: no
+* Default: `false`
+
#### `--verbose|-v|-vv|-vvv`
Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
@@ -399,7 +404,7 @@ Display help for the given command. When no command is given display help for th
* Is negatable: no
* Default: `false`
-#### `--quiet|-q`
+#### `--silent`
Do not output any message
@@ -409,6 +414,16 @@ Do not output any message
* Is negatable: no
* Default: `false`
+#### `--quiet|-q`
+
+Only errors are displayed. All other output is suppressed
+
+* Accept value: no
+* Is value required: no
+* Is multiple: no
+* Is negatable: no
+* Default: `false`
+
#### `--verbose|-v|-vv|-vvv`
Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
@@ -490,7 +505,7 @@ Display help for the given command. When no command is given display help for th
* Is negatable: no
* Default: `false`
-#### `--quiet|-q`
+#### `--silent`
Do not output any message
@@ -500,6 +515,16 @@ Do not output any message
* Is negatable: no
* Default: `false`
+#### `--quiet|-q`
+
+Only errors are displayed. All other output is suppressed
+
+* Accept value: no
+* Is value required: no
+* Is multiple: no
+* Is negatable: no
+* Default: `false`
+
#### `--verbose|-v|-vv|-vvv`
Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
@@ -562,7 +587,7 @@ Display help for the given command. When no command is given display help for th
* Is negatable: no
* Default: `false`
-#### `--quiet|-q`
+#### `--silent`
Do not output any message
@@ -572,6 +597,16 @@ Do not output any message
* Is negatable: no
* Default: `false`
+#### `--quiet|-q`
+
+Only errors are displayed. All other output is suppressed
+
+* Accept value: no
+* Is value required: no
+* Is multiple: no
+* Is negatable: no
+* Default: `false`
+
#### `--verbose|-v|-vv|-vvv`
Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
diff --git a/Tests/Fixtures/application_2.rst b/Tests/Fixtures/application_2.rst
new file mode 100644
index 000000000..6426b62bd
--- /dev/null
+++ b/Tests/Fixtures/application_2.rst
@@ -0,0 +1,216 @@
+My Symfony application v1.0
+===========================
+
+Table of Contents
+-----------------
+
+
+
+- `help`_
+- `list`_
+
+descriptor
+~~~~~~~~~~
+
+
+
+- `descriptor:command1`_
+- `descriptor:command2`_
+- `descriptor:command4`_
+
+Commands
+--------
+
+Global
+~~~~~~
+
+help
+....
+
+Display help for a command
+
+Usage
+^^^^^
+
+- ``help [--format FORMAT] [--raw] [--] []``
+
+The help command displays help for a given command:
+
+ %%PHP_SELF%% help list
+
+You can also output the help in other formats by using the --format option:
+
+ %%PHP_SELF%% help --format=xml list
+
+To display the list of available commands, please use the list command.
+
+Arguments
+^^^^^^^^^
+
+command_name
+
+Options
+^^^^^^^
+
+\-\-format
+""""""""""
+
+The output format (txt, xml, json, or md)
+
+- **Accept value**: yes
+- **Is value required**: yes
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``'txt'``
+
+\-\-raw
+"""""""
+
+To output raw command help
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
+
+
+
+list
+....
+
+List commands
+
+Usage
+^^^^^
+
+- ``list [--raw] [--format FORMAT] [--short] [--] []``
+
+The list command lists all commands:
+
+ %%PHP_SELF%% list
+
+You can also display the commands for a specific namespace:
+
+ %%PHP_SELF%% list test
+
+You can also output the information in other formats by using the --format option:
+
+ %%PHP_SELF%% list --format=xml
+
+It's also possible to get raw list of commands (useful for embedding command runner):
+
+ %%PHP_SELF%% list --raw
+
+Arguments
+^^^^^^^^^
+
+namespace
+
+Options
+^^^^^^^
+
+\-\-raw
+"""""""
+
+To output raw command list
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
+
+\-\-format
+""""""""""
+
+The output format (txt, xml, json, or md)
+
+- **Accept value**: yes
+- **Is value required**: yes
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``'txt'``
+
+\-\-short
+"""""""""
+
+To skip describing commands' arguments
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
+
+
+
+descriptor
+~~~~~~~~~~
+
+.. _alias1:
+
+.. _alias2:
+
+descriptor:command1
+...................
+
+command 1 description
+
+Usage
+^^^^^
+
+- ``descriptor:command1``
+- ``alias1``
+- ``alias2``
+
+command 1 help
+
+
+
+descriptor:command2
+...................
+
+command 2 description
+
+Usage
+^^^^^
+
+- ``descriptor:command2 [-o|--option_name] [--] ``
+- ``descriptor:command2 -o|--option_name ``
+- ``descriptor:command2 ``
+
+command 2 help
+
+Arguments
+^^^^^^^^^
+
+argument_name
+
+Options
+^^^^^^^
+
+\-\-option_name|-o
+""""""""""""""""""
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
+
+
+
+.. _descriptor:alias_command4:
+
+.. _command4:descriptor:
+
+descriptor:command4
+...................
+
+Usage
+^^^^^
+
+- ``descriptor:command4``
+- ``descriptor:alias_command4``
+- ``command4:descriptor``
diff --git a/Tests/Fixtures/application_2.txt b/Tests/Fixtures/application_2.txt
index aed535fa4..1725b5fa6 100644
--- a/Tests/Fixtures/application_2.txt
+++ b/Tests/Fixtures/application_2.txt
@@ -5,7 +5,8 @@ My Symfony application v1.0
Options:
-h, --help Display help for the given command. When no command is given display help for the list command
- -q, --quiet Do not output any message
+ --silent Do not output any message
+ -q, --quiet Only errors are displayed. All other output is suppressed
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
diff --git a/Tests/Fixtures/application_2.xml b/Tests/Fixtures/application_2.xml
index a217999a8..dd4b1800a 100644
--- a/Tests/Fixtures/application_2.xml
+++ b/Tests/Fixtures/application_2.xml
@@ -3,14 +3,14 @@
- _complete [-s|--shell SHELL] [-i|--input INPUT] [-c|--current CURRENT] [-S|--symfony SYMFONY]
+ _complete [-s|--shell SHELL] [-i|--input INPUT] [-c|--current CURRENT] [-a|--api-version API-VERSION] [-S|--symfony SYMFONY]
Internal command to provide shell completion suggestions
Internal command to provide shell completion suggestions
+
-
+
@@ -53,32 +60,7 @@
completion [--debug] [--] [<shell>]
Dump the shell completion script
- The <info>completion</> command dumps the shell completion script required
- to use shell autocompletion (currently only bash completion is supported).
-
- <comment>Static installation
- -------------------</>
-
- Dump the script to a global completion file and restart your shell:
-
- <info>%%PHP_SELF%% completion bash | sudo tee /etc/bash_completion.d/%%COMMAND_NAME%%</>
-
- Or dump the script to a local file and source it:
-
- <info>%%PHP_SELF%% completion bash > completion.sh</>
-
- <comment># source the file whenever you use the project</>
- <info>source completion.sh</>
-
- <comment># or add this line at the end of your "~/.bashrc" file:</>
- <info>source /path/to/completion.sh</>
-
- <comment>Dynamic installation
- --------------------</>
-
- Add this to the end of your shell configuration file (e.g. <info>"~/.bashrc"</>):
-
- <info>eval "$(%%PHP_SELF_FULL%% completion bash)"</>
+ Dump the shell completion script
The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given
@@ -92,9 +74,12 @@
-
+
@@ -147,9 +132,12 @@
-
+
@@ -209,9 +197,12 @@
-
+
@@ -242,9 +233,12 @@
-
+
@@ -283,9 +277,12 @@
-
+
@@ -314,9 +311,12 @@
-
+
@@ -347,9 +347,12 @@
-
+
diff --git a/Tests/Fixtures/application_filtered_namespace.txt b/Tests/Fixtures/application_filtered_namespace.txt
index c24da0bbc..762a7f68d 100644
--- a/Tests/Fixtures/application_filtered_namespace.txt
+++ b/Tests/Fixtures/application_filtered_namespace.txt
@@ -5,7 +5,8 @@ My Symfony application v1.0
Options:
-h, --help Display help for the given command. When no command is given display help for the list command
- -q, --quiet Do not output any message
+ --silent Do not output any message
+ -q, --quiet Only errors are displayed. All other output is suppressed
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
diff --git a/Tests/Fixtures/application_mbstring.md b/Tests/Fixtures/application_mbstring.md
index 740ea5c20..5e31b7ef4 100644
--- a/Tests/Fixtures/application_mbstring.md
+++ b/Tests/Fixtures/application_mbstring.md
@@ -18,32 +18,7 @@ Dump the shell completion script
* `completion [--debug] [--] []`
-The completion command dumps the shell completion script required
-to use shell autocompletion (currently only bash completion is supported).
-
-Static installation
--------------------
-
-Dump the script to a global completion file and restart your shell:
-
- %%PHP_SELF%% completion bash | sudo tee /etc/bash_completion.d/%%COMMAND_NAME%%
-
-Or dump the script to a local file and source it:
-
- %%PHP_SELF%% completion bash > completion.sh
-
- # source the file whenever you use the project
- source completion.sh
-
- # or add this line at the end of your "~/.bashrc" file:
- source /path/to/completion.sh
-
-Dynamic installation
---------------------
-
-Add this to the end of your shell configuration file (e.g. "~/.bashrc"):
-
- eval "$(%%PHP_SELF_FULL%% completion bash)"
+Dump the shell completion script
### Arguments
@@ -77,7 +52,7 @@ Display help for the given command. When no command is given display help for th
* Is negatable: no
* Default: `false`
-#### `--quiet|-q`
+#### `--silent`
Do not output any message
@@ -87,6 +62,16 @@ Do not output any message
* Is negatable: no
* Default: `false`
+#### `--quiet|-q`
+
+Only errors are displayed. All other output is suppressed
+
+* Accept value: no
+* Is value required: no
+* Is multiple: no
+* Is negatable: no
+* Default: `false`
+
#### `--verbose|-v|-vv|-vvv`
Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
@@ -188,7 +173,7 @@ Display help for the given command. When no command is given display help for th
* Is negatable: no
* Default: `false`
-#### `--quiet|-q`
+#### `--silent`
Do not output any message
@@ -198,6 +183,16 @@ Do not output any message
* Is negatable: no
* Default: `false`
+#### `--quiet|-q`
+
+Only errors are displayed. All other output is suppressed
+
+* Accept value: no
+* Is value required: no
+* Is multiple: no
+* Is negatable: no
+* Default: `false`
+
#### `--verbose|-v|-vv|-vvv`
Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
@@ -315,7 +310,7 @@ Display help for the given command. When no command is given display help for th
* Is negatable: no
* Default: `false`
-#### `--quiet|-q`
+#### `--silent`
Do not output any message
@@ -325,6 +320,16 @@ Do not output any message
* Is negatable: no
* Default: `false`
+#### `--quiet|-q`
+
+Only errors are displayed. All other output is suppressed
+
+* Accept value: no
+* Is value required: no
+* Is multiple: no
+* Is negatable: no
+* Default: `false`
+
#### `--verbose|-v|-vv|-vvv`
Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
@@ -406,7 +411,7 @@ Display help for the given command. When no command is given display help for th
* Is negatable: no
* Default: `false`
-#### `--quiet|-q`
+#### `--silent`
Do not output any message
@@ -416,6 +421,16 @@ Do not output any message
* Is negatable: no
* Default: `false`
+#### `--quiet|-q`
+
+Only errors are displayed. All other output is suppressed
+
+* Accept value: no
+* Is value required: no
+* Is multiple: no
+* Is negatable: no
+* Default: `false`
+
#### `--verbose|-v|-vv|-vvv`
Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
diff --git a/Tests/Fixtures/application_mbstring.rst b/Tests/Fixtures/application_mbstring.rst
new file mode 100644
index 000000000..3ea1ebfd5
--- /dev/null
+++ b/Tests/Fixtures/application_mbstring.rst
@@ -0,0 +1,178 @@
+MbString åpplicätion
+====================
+
+Table of Contents
+-----------------
+
+
+
+- `help`_
+- `list`_
+
+descriptor
+~~~~~~~~~~
+
+
+
+- `descriptor:åèä`_
+
+Commands
+--------
+
+Global
+~~~~~~
+
+help
+....
+
+Display help for a command
+
+Usage
+^^^^^
+
+- ``help [--format FORMAT] [--raw] [--] []``
+
+The help command displays help for a given command:
+
+ %%PHP_SELF%% help list
+
+You can also output the help in other formats by using the --format option:
+
+ %%PHP_SELF%% help --format=xml list
+
+To display the list of available commands, please use the list command.
+
+Arguments
+^^^^^^^^^
+
+command_name
+
+Options
+^^^^^^^
+
+\-\-format
+""""""""""
+
+The output format (txt, xml, json, or md)
+
+- **Accept value**: yes
+- **Is value required**: yes
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``'txt'``
+
+\-\-raw
+"""""""
+
+To output raw command help
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
+
+
+
+list
+....
+
+List commands
+
+Usage
+^^^^^
+
+- ``list [--raw] [--format FORMAT] [--short] [--] []``
+
+The list command lists all commands:
+
+ %%PHP_SELF%% list
+
+You can also display the commands for a specific namespace:
+
+ %%PHP_SELF%% list test
+
+You can also output the information in other formats by using the --format option:
+
+ %%PHP_SELF%% list --format=xml
+
+It's also possible to get raw list of commands (useful for embedding command runner):
+
+ %%PHP_SELF%% list --raw
+
+Arguments
+^^^^^^^^^
+
+namespace
+
+Options
+^^^^^^^
+
+\-\-raw
+"""""""
+
+To output raw command list
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
+
+\-\-format
+""""""""""
+
+The output format (txt, xml, json, or md)
+
+- **Accept value**: yes
+- **Is value required**: yes
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``'txt'``
+
+\-\-short
+"""""""""
+
+To skip describing commands' arguments
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
+
+
+
+descriptor
+~~~~~~~~~~
+
+descriptor:åèä
+..............
+
+command åèä description
+
+Usage
+^^^^^
+
+- ``descriptor:åèä [-o|--option_åèä] [--] ``
+- ``descriptor:åèä -o|--option_name ``
+- ``descriptor:åèä ``
+
+command åèä help
+
+Arguments
+^^^^^^^^^
+
+argument_åèä
+
+Options
+^^^^^^^
+
+\-\-option_åèä|-o
+"""""""""""""""""
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
diff --git a/Tests/Fixtures/application_mbstring.txt b/Tests/Fixtures/application_mbstring.txt
index 73a47fff4..e904ddf05 100644
--- a/Tests/Fixtures/application_mbstring.txt
+++ b/Tests/Fixtures/application_mbstring.txt
@@ -5,7 +5,8 @@ MbString åpplicätion
Options:
-h, --help Display help for the given command. When no command is given display help for the list command
- -q, --quiet Do not output any message
+ --silent Do not output any message
+ -q, --quiet Only errors are displayed. All other output is suppressed
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
diff --git a/Tests/Fixtures/application_run1.txt b/Tests/Fixtures/application_run1.txt
index 0b24a777c..2d6f6c666 100644
--- a/Tests/Fixtures/application_run1.txt
+++ b/Tests/Fixtures/application_run1.txt
@@ -5,7 +5,8 @@ Usage:
Options:
-h, --help Display help for the given command. When no command is given display help for the list command
- -q, --quiet Do not output any message
+ --silent Do not output any message
+ -q, --quiet Only errors are displayed. All other output is suppressed
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
diff --git a/Tests/Fixtures/application_run2.txt b/Tests/Fixtures/application_run2.txt
index ccd73d14c..8523e16a6 100644
--- a/Tests/Fixtures/application_run2.txt
+++ b/Tests/Fixtures/application_run2.txt
@@ -12,7 +12,8 @@ Options:
--format=FORMAT The output format (txt, xml, json, or md) [default: "txt"]
--short To skip describing commands' arguments
-h, --help Display help for the given command. When no command is given display help for the list command
- -q, --quiet Do not output any message
+ --silent Do not output any message
+ -q, --quiet Only errors are displayed. All other output is suppressed
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
diff --git a/Tests/Fixtures/application_run3.txt b/Tests/Fixtures/application_run3.txt
index ccd73d14c..8523e16a6 100644
--- a/Tests/Fixtures/application_run3.txt
+++ b/Tests/Fixtures/application_run3.txt
@@ -12,7 +12,8 @@ Options:
--format=FORMAT The output format (txt, xml, json, or md) [default: "txt"]
--short To skip describing commands' arguments
-h, --help Display help for the given command. When no command is given display help for the list command
- -q, --quiet Do not output any message
+ --silent Do not output any message
+ -q, --quiet Only errors are displayed. All other output is suppressed
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
diff --git a/Tests/Fixtures/application_run5.txt b/Tests/Fixtures/application_run5.txt
index de3fdd346..c5696492d 100644
--- a/Tests/Fixtures/application_run5.txt
+++ b/Tests/Fixtures/application_run5.txt
@@ -11,7 +11,8 @@ Options:
--format=FORMAT The output format (txt, xml, json, or md) [default: "txt"]
--raw To output raw command help
-h, --help Display help for the given command. When no command is given display help for the list command
- -q, --quiet Do not output any message
+ --silent Do not output any message
+ -q, --quiet Only errors are displayed. All other output is suppressed
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
diff --git a/Tests/Fixtures/application_signalable.php b/Tests/Fixtures/application_signalable.php
index 0194703b2..978406637 100644
--- a/Tests/Fixtures/application_signalable.php
+++ b/Tests/Fixtures/application_signalable.php
@@ -1,8 +1,6 @@
setCode(function(InputInterface $input, OutputInterface $output) {
diff --git a/Tests/Fixtures/command_1.rst b/Tests/Fixtures/command_1.rst
new file mode 100644
index 000000000..a4d93a3dc
--- /dev/null
+++ b/Tests/Fixtures/command_1.rst
@@ -0,0 +1,17 @@
+.. _alias1:
+
+.. _alias2:
+
+descriptor:command1
+...................
+
+command 1 description
+
+Usage
+^^^^^
+
+- ``descriptor:command1``
+- ``alias1``
+- ``alias2``
+
+command 1 help
diff --git a/Tests/Fixtures/command_2.rst b/Tests/Fixtures/command_2.rst
new file mode 100644
index 000000000..3744aad78
--- /dev/null
+++ b/Tests/Fixtures/command_2.rst
@@ -0,0 +1,30 @@
+descriptor:command2
+...................
+
+command 2 description
+
+Usage
+^^^^^
+
+- ``descriptor:command2 [-o|--option_name] [--] ``
+- ``descriptor:command2 -o|--option_name ``
+- ``descriptor:command2 ``
+
+command 2 help
+
+Arguments
+^^^^^^^^^
+
+argument_name
+
+Options
+^^^^^^^
+
+\-\-option_name|-o
+""""""""""""""""""
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
diff --git a/Tests/Fixtures/command_mbstring.rst b/Tests/Fixtures/command_mbstring.rst
new file mode 100644
index 000000000..d61163c2d
--- /dev/null
+++ b/Tests/Fixtures/command_mbstring.rst
@@ -0,0 +1,30 @@
+descriptor:åèä
+..............
+
+command åèä description
+
+Usage
+^^^^^
+
+- ``descriptor:åèä [-o|--option_åèä] [--] ``
+- ``descriptor:åèä -o|--option_name ``
+- ``descriptor:åèä ``
+
+command åèä help
+
+Arguments
+^^^^^^^^^
+
+argument_åèä
+
+Options
+^^^^^^^
+
+\-\-option_åèä|-o
+"""""""""""""""""
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
diff --git a/Tests/Fixtures/input_argument_1.rst b/Tests/Fixtures/input_argument_1.rst
new file mode 100644
index 000000000..4db1cd215
--- /dev/null
+++ b/Tests/Fixtures/input_argument_1.rst
@@ -0,0 +1 @@
+argument_name
diff --git a/Tests/Fixtures/input_argument_2.rst b/Tests/Fixtures/input_argument_2.rst
new file mode 100644
index 000000000..4db1cd215
--- /dev/null
+++ b/Tests/Fixtures/input_argument_2.rst
@@ -0,0 +1 @@
+argument_name
diff --git a/Tests/Fixtures/input_argument_3.rst b/Tests/Fixtures/input_argument_3.rst
new file mode 100644
index 000000000..4db1cd215
--- /dev/null
+++ b/Tests/Fixtures/input_argument_3.rst
@@ -0,0 +1 @@
+argument_name
diff --git a/Tests/Fixtures/input_argument_4.rst b/Tests/Fixtures/input_argument_4.rst
new file mode 100644
index 000000000..4db1cd215
--- /dev/null
+++ b/Tests/Fixtures/input_argument_4.rst
@@ -0,0 +1 @@
+argument_name
diff --git a/Tests/Fixtures/input_argument_with_default_inf_value.rst b/Tests/Fixtures/input_argument_with_default_inf_value.rst
new file mode 100644
index 000000000..4db1cd215
--- /dev/null
+++ b/Tests/Fixtures/input_argument_with_default_inf_value.rst
@@ -0,0 +1 @@
+argument_name
diff --git a/Tests/Fixtures/input_argument_with_style.rst b/Tests/Fixtures/input_argument_with_style.rst
new file mode 100644
index 000000000..4db1cd215
--- /dev/null
+++ b/Tests/Fixtures/input_argument_with_style.rst
@@ -0,0 +1 @@
+argument_name
diff --git a/Tests/Fixtures/input_definition_1.rst b/Tests/Fixtures/input_definition_1.rst
new file mode 100644
index 000000000..e69de29bb
diff --git a/Tests/Fixtures/input_definition_2.rst b/Tests/Fixtures/input_definition_2.rst
new file mode 100644
index 000000000..0f5b76018
--- /dev/null
+++ b/Tests/Fixtures/input_definition_2.rst
@@ -0,0 +1,4 @@
+Arguments
+^^^^^^^^^
+
+argument_name
diff --git a/Tests/Fixtures/input_definition_3.rst b/Tests/Fixtures/input_definition_3.rst
new file mode 100644
index 000000000..1a5e9b1ad
--- /dev/null
+++ b/Tests/Fixtures/input_definition_3.rst
@@ -0,0 +1,11 @@
+Options
+^^^^^^^
+
+\-\-option_name|-o
+""""""""""""""""""
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
diff --git a/Tests/Fixtures/input_definition_4.rst b/Tests/Fixtures/input_definition_4.rst
new file mode 100644
index 000000000..1c65681ab
--- /dev/null
+++ b/Tests/Fixtures/input_definition_4.rst
@@ -0,0 +1,16 @@
+Arguments
+^^^^^^^^^
+
+argument_name
+
+Options
+^^^^^^^
+
+\-\-option_name|-o
+""""""""""""""""""
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
diff --git a/Tests/Fixtures/input_option_1.rst b/Tests/Fixtures/input_option_1.rst
new file mode 100644
index 000000000..93662791f
--- /dev/null
+++ b/Tests/Fixtures/input_option_1.rst
@@ -0,0 +1,8 @@
+\-\-option_name|-o
+""""""""""""""""""
+
+- **Accept value**: no
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``false``
diff --git a/Tests/Fixtures/input_option_2.rst b/Tests/Fixtures/input_option_2.rst
new file mode 100644
index 000000000..0a8a14c66
--- /dev/null
+++ b/Tests/Fixtures/input_option_2.rst
@@ -0,0 +1,10 @@
+\-\-option_name|-o
+""""""""""""""""""
+
+option description
+
+- **Accept value**: yes
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``'default_value'``
diff --git a/Tests/Fixtures/input_option_3.rst b/Tests/Fixtures/input_option_3.rst
new file mode 100644
index 000000000..45374910c
--- /dev/null
+++ b/Tests/Fixtures/input_option_3.rst
@@ -0,0 +1,10 @@
+\-\-option_name|-o
+""""""""""""""""""
+
+option description
+
+- **Accept value**: yes
+- **Is value required**: yes
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``NULL``
diff --git a/Tests/Fixtures/input_option_4.rst b/Tests/Fixtures/input_option_4.rst
new file mode 100644
index 000000000..fe81fc1fe
--- /dev/null
+++ b/Tests/Fixtures/input_option_4.rst
@@ -0,0 +1,10 @@
+\-\-option_name|-o
+""""""""""""""""""
+
+option description
+
+- **Accept value**: yes
+- **Is value required**: no
+- **Is multiple**: yes
+- **Is negatable**: no
+- **Default**: ``array ()``
diff --git a/Tests/Fixtures/input_option_5.rst b/Tests/Fixtures/input_option_5.rst
new file mode 100644
index 000000000..c2b6a4cd7
--- /dev/null
+++ b/Tests/Fixtures/input_option_5.rst
@@ -0,0 +1,12 @@
+\-\-option_name|-o
+""""""""""""""""""
+
+multiline
+
+option description
+
+- **Accept value**: yes
+- **Is value required**: yes
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``NULL``
diff --git a/Tests/Fixtures/input_option_6.rst b/Tests/Fixtures/input_option_6.rst
new file mode 100644
index 000000000..748cad720
--- /dev/null
+++ b/Tests/Fixtures/input_option_6.rst
@@ -0,0 +1,10 @@
+\-\-option_name|-o|-O
+"""""""""""""""""""""
+
+option with multiple shortcuts
+
+- **Accept value**: yes
+- **Is value required**: yes
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``NULL``
diff --git a/Tests/Fixtures/input_option_with_default_inf_value.rst b/Tests/Fixtures/input_option_with_default_inf_value.rst
new file mode 100644
index 000000000..8a99db1ba
--- /dev/null
+++ b/Tests/Fixtures/input_option_with_default_inf_value.rst
@@ -0,0 +1,10 @@
+\-\-option_name|-o
+""""""""""""""""""
+
+option description
+
+- **Accept value**: yes
+- **Is value required**: no
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``INF``
diff --git a/Tests/Fixtures/input_option_with_style.rst b/Tests/Fixtures/input_option_with_style.rst
new file mode 100644
index 000000000..47a9886ee
--- /dev/null
+++ b/Tests/Fixtures/input_option_with_style.rst
@@ -0,0 +1,10 @@
+\-\-option_name|-o
+""""""""""""""""""
+
+option description
+
+- **Accept value**: yes
+- **Is value required**: yes
+- **Is multiple**: no
+- **Is negatable**: no
+- **Default**: ``'style'``
diff --git a/Tests/Fixtures/input_option_with_style_array.rst b/Tests/Fixtures/input_option_with_style_array.rst
new file mode 100644
index 000000000..29a822e56
--- /dev/null
+++ b/Tests/Fixtures/input_option_with_style_array.rst
@@ -0,0 +1,10 @@
+\-\-option_name|-o
+""""""""""""""""""
+
+option description
+
+- **Accept value**: yes
+- **Is value required**: yes
+- **Is multiple**: yes
+- **Is negatable**: no
+- **Default**: ``array ( 0 => 'Hello', 1 => 'world',)``
diff --git a/Tests/Formatter/OutputFormatterStyleStackTest.php b/Tests/Formatter/OutputFormatterStyleStackTest.php
index 7fbe4f415..0ceab34ea 100644
--- a/Tests/Formatter/OutputFormatterStyleStackTest.php
+++ b/Tests/Formatter/OutputFormatterStyleStackTest.php
@@ -61,9 +61,11 @@ public function testPopNotLast()
public function testInvalidPop()
{
- $this->expectException(\InvalidArgumentException::class);
$stack = new OutputFormatterStyleStack();
$stack->push(new OutputFormatterStyle('white', 'black'));
+
+ $this->expectException(\InvalidArgumentException::class);
+
$stack->pop(new OutputFormatterStyle('yellow', 'blue'));
}
}
diff --git a/Tests/Formatter/OutputFormatterTest.php b/Tests/Formatter/OutputFormatterTest.php
index f65e0a15d..477f1bdf6 100644
--- a/Tests/Formatter/OutputFormatterTest.php
+++ b/Tests/Formatter/OutputFormatterTest.php
@@ -171,7 +171,6 @@ public function testInlineStyleOptions(string $tag, ?string $expected = null, ?s
$styleString = substr($tag, 1, -1);
$formatter = new OutputFormatter(true);
$method = new \ReflectionMethod($formatter, 'createStyleFromString');
- $method->setAccessible(true);
$result = $method->invoke($formatter, $styleString);
if (null === $expected) {
$this->assertNull($result);
@@ -245,12 +244,8 @@ public function testFormatterHasStyles()
/**
* @dataProvider provideDecoratedAndNonDecoratedOutput
*/
- public function testNotDecoratedFormatterOnJediTermEmulator(
- string $input,
- string $expectedNonDecoratedOutput,
- string $expectedDecoratedOutput,
- bool $shouldBeJediTerm = false
- ) {
+ public function testNotDecoratedFormatterOnJediTermEmulator(string $input, string $expectedNonDecoratedOutput, string $expectedDecoratedOutput, bool $shouldBeJediTerm = false)
+ {
$terminalEmulator = $shouldBeJediTerm ? 'JetBrains-JediTerm' : 'Unknown';
$prevTerminalEmulator = getenv('TERMINAL_EMULATOR');
@@ -267,12 +262,8 @@ public function testNotDecoratedFormatterOnJediTermEmulator(
/**
* @dataProvider provideDecoratedAndNonDecoratedOutput
*/
- public function testNotDecoratedFormatterOnIDEALikeEnvironment(
- string $input,
- string $expectedNonDecoratedOutput,
- string $expectedDecoratedOutput,
- bool $expectsIDEALikeTerminal = false
- ) {
+ public function testNotDecoratedFormatterOnIDEALikeEnvironment(string $input, string $expectedNonDecoratedOutput, string $expectedDecoratedOutput, bool $expectsIDEALikeTerminal = false)
+ {
// Backup previous env variable
$previousValue = $_SERVER['IDEA_INITIAL_DIRECTORY'] ?? null;
$hasPreviousValue = \array_key_exists('IDEA_INITIAL_DIRECTORY', $_SERVER);
diff --git a/Tests/Helper/DescriptorHelperTest.php b/Tests/Helper/DescriptorHelperTest.php
index 97bae77c5..57435a3e7 100644
--- a/Tests/Helper/DescriptorHelperTest.php
+++ b/Tests/Helper/DescriptorHelperTest.php
@@ -24,6 +24,7 @@ public function testGetFormats()
'xml',
'json',
'md',
+ 'rst',
];
$this->assertSame($expectedFormats, $helper->getFormats());
}
diff --git a/Tests/Helper/HelperSetTest.php b/Tests/Helper/HelperSetTest.php
index c83b9d5a3..389ee0ed3 100644
--- a/Tests/Helper/HelperSetTest.php
+++ b/Tests/Helper/HelperSetTest.php
@@ -12,7 +12,6 @@
namespace Symfony\Component\Console\Tests\Helper;
use PHPUnit\Framework\TestCase;
-use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Helper\HelperInterface;
use Symfony\Component\Console\Helper\HelperSet;
@@ -74,35 +73,6 @@ public function testGet()
}
}
- /**
- * @group legacy
- */
- public function testSetCommand()
- {
- $cmd_01 = new Command('foo');
- $cmd_02 = new Command('bar');
-
- $helperset = new HelperSet();
- $helperset->setCommand($cmd_01);
- $this->assertEquals($cmd_01, $helperset->getCommand(), '->setCommand() stores given command');
-
- $helperset = new HelperSet();
- $helperset->setCommand($cmd_01);
- $helperset->setCommand($cmd_02);
- $this->assertEquals($cmd_02, $helperset->getCommand(), '->setCommand() overwrites stored command with consecutive calls');
- }
-
- /**
- * @group legacy
- */
- public function testGetCommand()
- {
- $cmd = new Command('foo');
- $helperset = new HelperSet();
- $helperset->setCommand($cmd);
- $this->assertEquals($cmd, $helperset->getCommand(), '->getCommand() retrieves stored command');
- }
-
public function testIteration()
{
$helperset = new HelperSet();
diff --git a/Tests/Helper/HelperTest.php b/Tests/Helper/HelperTest.php
index 9f59aa2ff..0a0c2fa48 100644
--- a/Tests/Helper/HelperTest.php
+++ b/Tests/Helper/HelperTest.php
@@ -20,26 +20,31 @@ class HelperTest extends TestCase
public static function formatTimeProvider()
{
return [
- [0, '< 1 sec'],
- [1, '1 sec'],
- [2, '2 secs'],
- [59, '59 secs'],
- [60, '1 min'],
- [61, '1 min'],
- [119, '1 min'],
- [120, '2 mins'],
- [121, '2 mins'],
- [3599, '59 mins'],
- [3600, '1 hr'],
- [7199, '1 hr'],
- [7200, '2 hrs'],
- [7201, '2 hrs'],
- [86399, '23 hrs'],
- [86400, '1 day'],
- [86401, '1 day'],
- [172799, '1 day'],
- [172800, '2 days'],
- [172801, '2 days'],
+ [0, '< 1 sec', 1],
+ [0.95, '< 1 sec', 1],
+ [1, '1 sec', 1],
+ [2, '2 secs', 2],
+ [59, '59 secs', 1],
+ [59.21, '59 secs', 1],
+ [60, '1 min', 2],
+ [61, '1 min, 1 sec', 2],
+ [119, '1 min, 59 secs', 2],
+ [120, '2 mins', 2],
+ [121, '2 mins, 1 sec', 2],
+ [3599, '59 mins, 59 secs', 2],
+ [3600, '1 hr', 2],
+ [7199, '1 hr, 59 mins', 2],
+ [7200, '2 hrs', 2],
+ [7201, '2 hrs', 2],
+ [86399, '23 hrs, 59 mins', 2],
+ [86399, '23 hrs, 59 mins, 59 secs', 3],
+ [86400, '1 day', 2],
+ [86401, '1 day', 2],
+ [172799, '1 day, 23 hrs', 2],
+ [172799, '1 day, 23 hrs, 59 mins, 59 secs', 4],
+ [172800, '2 days', 2],
+ [172801, '2 days', 2],
+ [172801, '2 days, 1 sec', 4],
];
}
@@ -55,13 +60,10 @@ public static function decoratedTextProvider()
/**
* @dataProvider formatTimeProvider
- *
- * @param int $secs
- * @param string $expectedFormat
*/
- public function testFormatTime($secs, $expectedFormat)
+ public function testFormatTime(int|float $secs, string $expectedFormat, int $precision)
{
- $this->assertEquals($expectedFormat, Helper::formatTime($secs));
+ $this->assertEquals($expectedFormat, Helper::formatTime($secs, $precision));
}
/**
diff --git a/Tests/Helper/OutputWrapperTest.php b/Tests/Helper/OutputWrapperTest.php
new file mode 100644
index 000000000..2ce15b6d4
--- /dev/null
+++ b/Tests/Helper/OutputWrapperTest.php
@@ -0,0 +1,73 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Tests\Helper;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Helper\OutputWrapper;
+
+class OutputWrapperTest extends TestCase
+{
+ /**
+ * @dataProvider textProvider
+ */
+ public function testBasicWrap(string $text, int $width, bool $allowCutUrls, string $expected)
+ {
+ $wrapper = new OutputWrapper($allowCutUrls);
+ $result = $wrapper->wrap($text, $width);
+ $this->assertEquals($expected, $result);
+ }
+
+ public static function textProvider(): iterable
+ {
+ $baseTextWithUtf8AndUrl = 'Árvíztűrőtükörfúrógép https://github.com/symfony/symfony Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent vestibulum nulla quis urna maximus porttitor. Donec ullamcorper risus at libero ornare efficitur.';
+
+ yield 'Default URL cut' => [
+ $baseTextWithUtf8AndUrl,
+ 20,
+ false,
+ <<<'EOS'
+ Árvíztűrőtükörfúrógé
+ p https://github.com/symfony/symfony Lorem ipsum
+ dolor sit amet,
+ consectetur
+ adipiscing elit.
+ Praesent vestibulum
+ nulla quis urna
+ maximus porttitor.
+ Donec ullamcorper
+ risus at libero
+ ornare efficitur.
+ EOS,
+ ];
+
+ yield 'Allow URL cut' => [
+ $baseTextWithUtf8AndUrl,
+ 20,
+ true,
+ <<<'EOS'
+ Árvíztűrőtükörfúrógé
+ p
+ https://github.com/s
+ ymfony/symfony Lorem
+ ipsum dolor sit
+ amet, consectetur
+ adipiscing elit.
+ Praesent vestibulum
+ nulla quis urna
+ maximus porttitor.
+ Donec ullamcorper
+ risus at libero
+ ornare efficitur.
+ EOS,
+ ];
+ }
+}
diff --git a/Tests/Helper/ProcessHelperTest.php b/Tests/Helper/ProcessHelperTest.php
index b4d7b0bbc..1fd88987b 100644
--- a/Tests/Helper/ProcessHelperTest.php
+++ b/Tests/Helper/ProcessHelperTest.php
@@ -23,10 +23,10 @@ class ProcessHelperTest extends TestCase
/**
* @dataProvider provideCommandsAndOutput
*/
- public function testVariousProcessRuns($expected, $cmd, $verbosity, $error)
+ public function testVariousProcessRuns(string $expected, Process|string|array $cmd, int $verbosity, ?string $error)
{
if (\is_string($cmd)) {
- $cmd = method_exists(Process::class, 'fromShellCommandline') ? Process::fromShellCommandline($cmd) : new Process($cmd);
+ $cmd = Process::fromShellCommandline($cmd);
}
$helper = new ProcessHelper();
@@ -49,7 +49,7 @@ public function testPassedCallbackIsExecuted()
$this->assertTrue($executed);
}
- public static function provideCommandsAndOutput()
+ public static function provideCommandsAndOutput(): array
{
$successOutputVerbose = <<<'EOT'
RUN php -r "echo 42;"
@@ -99,7 +99,6 @@ public static function provideCommandsAndOutput()
$args = new Process(['php', '-r', 'echo 42;']);
$args = $args->getCommandLine();
$successOutputProcessDebug = str_replace("'php' '-r' 'echo 42;'", $args, $successOutputProcessDebug);
- $fromShellCommandline = method_exists(Process::class, 'fromShellCommandline') ? [Process::class, 'fromShellCommandline'] : function ($cmd) { return new Process($cmd); };
return [
['', 'php -r "echo 42;"', StreamOutput::VERBOSITY_VERBOSE, null],
@@ -113,18 +112,18 @@ public static function provideCommandsAndOutput()
[$syntaxErrorOutputVerbose.$errorMessage.\PHP_EOL, 'php -r "fwrite(STDERR, \'error message\');usleep(50000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_VERY_VERBOSE, $errorMessage],
[$syntaxErrorOutputDebug.$errorMessage.\PHP_EOL, 'php -r "fwrite(STDERR, \'error message\');usleep(500000);fwrite(STDOUT, \'out message\');exit(252);"', StreamOutput::VERBOSITY_DEBUG, $errorMessage],
[$successOutputProcessDebug, ['php', '-r', 'echo 42;'], StreamOutput::VERBOSITY_DEBUG, null],
- [$successOutputDebug, $fromShellCommandline('php -r "echo 42;"'), StreamOutput::VERBOSITY_DEBUG, null],
+ [$successOutputDebug, Process::fromShellCommandline('php -r "echo 42;"'), StreamOutput::VERBOSITY_DEBUG, null],
[$successOutputProcessDebug, [new Process(['php', '-r', 'echo 42;'])], StreamOutput::VERBOSITY_DEBUG, null],
- [$successOutputPhp, [$fromShellCommandline('php -r '.$PHP), 'PHP' => 'echo 42;'], StreamOutput::VERBOSITY_DEBUG, null],
+ [$successOutputPhp, [Process::fromShellCommandline('php -r '.$PHP), 'PHP' => 'echo 42;'], StreamOutput::VERBOSITY_DEBUG, null],
];
}
- private function getOutputStream($verbosity)
+ private function getOutputStream($verbosity): StreamOutput
{
return new StreamOutput(fopen('php://memory', 'r+', false), $verbosity, false);
}
- private function getOutput(StreamOutput $output)
+ private function getOutput(StreamOutput $output): string
{
rewind($output->getStream());
diff --git a/Tests/Helper/ProgressBarTest.php b/Tests/Helper/ProgressBarTest.php
index 901bca630..0df42e738 100644
--- a/Tests/Helper/ProgressBarTest.php
+++ b/Tests/Helper/ProgressBarTest.php
@@ -23,7 +23,7 @@
*/
class ProgressBarTest extends TestCase
{
- private $colSize;
+ private string|false $colSize;
protected function setUp(): void
{
@@ -66,6 +66,79 @@ public function testAdvance()
);
}
+ public function testResumeNoMax()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(), 0, 0);
+ $bar->start(null, 15);
+ $bar->advance();
+
+ rewind($output->getStream());
+
+ $this->assertEquals(
+ ' 15 [--------------->------------]'.
+ $this->generateOutput(' 16 [---------------->-----------]'),
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testResumeWithMax()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(), 5000, 0);
+ $bar->start(null, 1000);
+
+ rewind($output->getStream());
+
+ $this->assertEquals(
+ ' 1000/5000 [=====>----------------------] 20%',
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testRegularTimeEstimation()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(), 1_200, 0);
+ $bar->start();
+
+ $bar->advance();
+ $bar->advance();
+
+ sleep(1);
+
+ $this->assertEquals(
+ 600.0,
+ $bar->getEstimated()
+ );
+ }
+
+ public function testRegularTimeRemainingWithDifferentStartAtAndCustomDisplay()
+ {
+ $this->expectNotToPerformAssertions();
+
+ ProgressBar::setFormatDefinition('custom', ' %current%/%max% [%bar%] %percent:3s%% %remaining% %estimated%');
+ $bar = new ProgressBar($this->getOutputStream(), 1_200, 0);
+ $bar->setFormat('custom');
+ $bar->start(1_200, 600);
+ }
+
+ public function testResumedTimeEstimation()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(), 1_200, 0);
+ $bar->start(null, 599);
+ $bar->advance();
+
+ sleep(1);
+
+ $this->assertEquals(
+ 1_200.0,
+ $bar->getEstimated()
+ );
+
+ $this->assertEquals(
+ 600.0,
+ $bar->getRemaining()
+ );
+ }
+
public function testAdvanceWithStep()
{
$bar = new ProgressBar($output = $this->getOutputStream(), 0, 0);
@@ -343,6 +416,81 @@ public function testOverwriteWithSectionOutput()
);
}
+ public function testOverwriteWithSectionOutputAndEol()
+ {
+ $sections = [];
+ $stream = $this->getOutputStream(true);
+ $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter());
+
+ $bar = new ProgressBar($output, 50, 0);
+ $bar->setFormat('[%bar%] %percent:3s%%' . PHP_EOL . '%message%' . PHP_EOL);
+ $bar->setMessage('');
+ $bar->start();
+ $bar->display();
+ $bar->setMessage('Doing something...');
+ $bar->advance();
+ $bar->setMessage('Doing something foo...');
+ $bar->advance();
+
+ rewind($output->getStream());
+ $this->assertEquals(escapeshellcmd(
+ '[>---------------------------] 0%'.\PHP_EOL.\PHP_EOL.
+ "\x1b[2A\x1b[0J".'[>---------------------------] 2%'.\PHP_EOL. 'Doing something...' . \PHP_EOL .
+ "\x1b[2A\x1b[0J".'[=>--------------------------] 4%'.\PHP_EOL. 'Doing something foo...' . \PHP_EOL),
+ escapeshellcmd(stream_get_contents($output->getStream()))
+ );
+ }
+
+ public function testOverwriteWithSectionOutputAndEolWithEmptyMessage()
+ {
+ $sections = [];
+ $stream = $this->getOutputStream(true);
+ $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter());
+
+ $bar = new ProgressBar($output, 50, 0);
+ $bar->setFormat('[%bar%] %percent:3s%%' . PHP_EOL . '%message%');
+ $bar->setMessage('Start');
+ $bar->start();
+ $bar->display();
+ $bar->setMessage('');
+ $bar->advance();
+ $bar->setMessage('Doing something...');
+ $bar->advance();
+
+ rewind($output->getStream());
+ $this->assertEquals(escapeshellcmd(
+ '[>---------------------------] 0%'.\PHP_EOL.'Start'.\PHP_EOL.
+ "\x1b[2A\x1b[0J".'[>---------------------------] 2%'.\PHP_EOL .
+ "\x1b[1A\x1b[0J".'[=>--------------------------] 4%'.\PHP_EOL. 'Doing something...' . \PHP_EOL),
+ escapeshellcmd(stream_get_contents($output->getStream()))
+ );
+ }
+
+ public function testOverwriteWithSectionOutputAndEolWithEmptyMessageComment()
+ {
+ $sections = [];
+ $stream = $this->getOutputStream(true);
+ $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter());
+
+ $bar = new ProgressBar($output, 50, 0);
+ $bar->setFormat('[%bar%] %percent:3s%%' . PHP_EOL . '%message%');
+ $bar->setMessage('Start');
+ $bar->start();
+ $bar->display();
+ $bar->setMessage('');
+ $bar->advance();
+ $bar->setMessage('Doing something...');
+ $bar->advance();
+
+ rewind($output->getStream());
+ $this->assertEquals(escapeshellcmd(
+ '[>---------------------------] 0%'.\PHP_EOL."\x1b[33mStart\x1b[39m".\PHP_EOL.
+ "\x1b[2A\x1b[0J".'[>---------------------------] 2%'.\PHP_EOL .
+ "\x1b[1A\x1b[0J".'[=>--------------------------] 4%'.\PHP_EOL. "\x1b[33mDoing something...\x1b[39m" . \PHP_EOL),
+ escapeshellcmd(stream_get_contents($output->getStream()))
+ );
+ }
+
public function testOverwriteWithAnsiSectionOutput()
{
// output has 43 visible characters plus 2 invisible ANSI characters
@@ -801,9 +949,7 @@ public function testWithSmallScreen()
public function testAddingPlaceholderFormatter()
{
- ProgressBar::setPlaceholderFormatterDefinition('remaining_steps', function (ProgressBar $bar) {
- return $bar->getMaxSteps() - $bar->getProgress();
- });
+ ProgressBar::setPlaceholderFormatterDefinition('remaining_steps', fn (ProgressBar $bar) => $bar->getMaxSteps() - $bar->getProgress());
$bar = new ProgressBar($output = $this->getOutputStream(), 3, 0);
$bar->setFormat(' %remaining_steps% [%bar%]');
@@ -820,6 +966,27 @@ public function testAddingPlaceholderFormatter()
);
}
+ public function testAddingInstancePlaceholderFormatter()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(), 3, 0);
+ $bar->setFormat(' %countdown% [%bar%]');
+ $bar->setPlaceholderFormatter('countdown', $function = fn (ProgressBar $bar) => $bar->getMaxSteps() - $bar->getProgress());
+
+ $this->assertSame($function, $bar->getPlaceholderFormatter('countdown'));
+
+ $bar->start();
+ $bar->advance();
+ $bar->finish();
+
+ rewind($output->getStream());
+ $this->assertEquals(
+ ' 3 [>---------------------------]'.
+ $this->generateOutput(' 2 [=========>------------------]').
+ $this->generateOutput(' 0 [============================]'),
+ stream_get_contents($output->getStream())
+ );
+ }
+
public function testMultilineFormat()
{
$bar = new ProgressBar($output = $this->getOutputStream(), 3, 0);
@@ -923,6 +1090,18 @@ public function testSetFormat()
);
}
+ public function testSetFormatWithTimes()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(), 15, 0);
+ $bar->setFormat('%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%/%remaining:-6s%');
+ $bar->start();
+ rewind($output->getStream());
+ $this->assertEquals(
+ ' 0/15 [>---------------------------] 0% < 1 sec/< 1 sec/< 1 sec',
+ stream_get_contents($output->getStream())
+ );
+ }
+
public function testUnicode()
{
$bar = new ProgressBar($output = $this->getOutputStream(), 10, 0);
@@ -998,6 +1177,20 @@ public function testIterateUncountable()
);
}
+ public function testEmptyInputWithDebugFormat()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream());
+ $bar->setFormat('%current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%');
+
+ $this->assertEquals([], iterator_to_array($bar->iterate([])));
+
+ rewind($output->getStream());
+ $this->assertEquals(
+ ' 0/0 [============================] 100% < 1 sec/< 1 sec',
+ stream_get_contents($output->getStream())
+ );
+ }
+
protected function getOutputStream($decorated = true, $verbosity = StreamOutput::VERBOSITY_NORMAL)
{
return new StreamOutput(fopen('php://memory', 'r+', false), $verbosity, $decorated);
@@ -1169,7 +1362,7 @@ public function testMultiLineFormatIsFullyCorrectlyWithManuallyCleanup()
'Foo!'.\PHP_EOL.
$this->generateOutput('[--->------------------------]').
"\nProcessing \"foobar\"...".
- $this->generateOutput("[----->----------------------]\nProcessing \"foobar\"..."),
+ $this->generateOutput("[============================]\nProcessing \"foobar\"..."),
stream_get_contents($output->getStream())
);
}
diff --git a/Tests/Helper/ProgressIndicatorTest.php b/Tests/Helper/ProgressIndicatorTest.php
index ffb3472ec..2a4441d57 100644
--- a/Tests/Helper/ProgressIndicatorTest.php
+++ b/Tests/Helper/ProgressIndicatorTest.php
@@ -54,11 +54,11 @@ public function testDefaultIndicator()
$this->generateOutput(' \\ Starting...').
$this->generateOutput(' \\ Advancing...').
$this->generateOutput(' | Advancing...').
- $this->generateOutput(' | Done...').
+ $this->generateOutput(' ✔ Done...').
\PHP_EOL.
$this->generateOutput(' - Starting Again...').
$this->generateOutput(' \\ Starting Again...').
- $this->generateOutput(' \\ Done Again...').
+ $this->generateOutput(' ✔ Done Again...').
\PHP_EOL,
stream_get_contents($output->getStream())
);
@@ -109,6 +109,39 @@ public function testCustomIndicatorValues()
);
}
+ public function testCustomFinishedIndicatorValue()
+ {
+ $bar = new ProgressIndicator($output = $this->getOutputStream(), null, 100, ['a', 'b'], '✅');
+
+ $bar->start('Starting...');
+ usleep(101000);
+ $bar->finish('Done');
+
+ rewind($output->getStream());
+
+ $this->assertSame(
+ $this->generateOutput(' a Starting...').
+ $this->generateOutput(' ✅ Done').\PHP_EOL,
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testCustomFinishedIndicatorWhenFinishingProcess()
+ {
+ $bar = new ProgressIndicator($output = $this->getOutputStream(), null, 100, ['a', 'b']);
+
+ $bar->start('Starting...');
+ $bar->finish('Process failed', '❌');
+
+ rewind($output->getStream());
+
+ $this->assertEquals(
+ $this->generateOutput(' a Starting...').
+ $this->generateOutput(' ❌ Process failed').\PHP_EOL,
+ stream_get_contents($output->getStream())
+ );
+ }
+
public function testCannotSetInvalidIndicatorCharacters()
{
$this->expectException(\InvalidArgumentException::class);
@@ -118,10 +151,12 @@ public function testCannotSetInvalidIndicatorCharacters()
public function testCannotStartAlreadyStartedIndicator()
{
- $this->expectException(\LogicException::class);
- $this->expectExceptionMessage('Progress indicator already started.');
$bar = new ProgressIndicator($this->getOutputStream());
$bar->start('Starting...');
+
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage('Progress indicator already started.');
+
$bar->start('Starting Again.');
}
@@ -177,6 +212,6 @@ protected function generateOutput($expected)
{
$count = substr_count($expected, "\n");
- return "\x0D\x1B[2K".($count ? sprintf("\033[%dA", $count) : '').$expected;
+ return "\x0D\x1B[2K".($count ? \sprintf("\033[%dA", $count) : '').$expected;
}
}
diff --git a/Tests/Helper/QuestionHelperTest.php b/Tests/Helper/QuestionHelperTest.php
index 06c89183e..42da50273 100644
--- a/Tests/Helper/QuestionHelperTest.php
+++ b/Tests/Helper/QuestionHelperTest.php
@@ -286,9 +286,7 @@ public function testAskWithAutocompleteCallback()
$suggestionBase = $inputWords ? implode(' ', $inputWords).' ' : '';
return array_map(
- function ($word) use ($suggestionBase) {
- return $suggestionBase.$word.' ';
- },
+ fn ($word) => $suggestionBase.$word.' ',
$knownWords
);
};
@@ -679,8 +677,6 @@ public function testSelectChoiceFromChoiceList($providedAnswer, $expectedValue)
public function testAmbiguousChoiceFromChoicelist()
{
- $this->expectException(\InvalidArgumentException::class);
- $this->expectExceptionMessage('The provided answer is ambiguous. Value should be one of "env_2" or "env_3".');
$possibleChoices = [
'env_1' => 'My first environment',
'env_2' => 'My environment',
@@ -694,10 +690,13 @@ public function testAmbiguousChoiceFromChoicelist()
$question = new ChoiceQuestion('Please select the environment to load', $possibleChoices);
$question->setMaxAttempts(1);
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('The provided answer is ambiguous. Value should be one of "env_2" or "env_3".');
+
$dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream("My environment\n")), $this->createOutputInterface(), $question);
}
- public static function answerProvider()
+ public static function answerProvider(): array
{
return [
['env_1', 'env_1'],
@@ -714,9 +713,6 @@ public function testNoInteraction()
$this->assertEquals('not yet', $dialog->ask($this->createStreamableInputInterfaceMock(null, false), $this->createOutputInterface(), $question));
}
- /**
- * @requires function mb_strwidth
- */
public function testChoiceOutputFormattingQuestionForUtf8Keys()
{
$question = 'Lorem ipsum?';
@@ -748,22 +744,18 @@ public function testAskThrowsExceptionOnMissingInput()
{
$this->expectException(MissingInputException::class);
$this->expectExceptionMessage('Aborted.');
- $dialog = new QuestionHelper();
- $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new Question('What\'s your name?'));
+ (new QuestionHelper())->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new Question('What\'s your name?'));
}
public function testAskThrowsExceptionOnMissingInputForChoiceQuestion()
{
$this->expectException(MissingInputException::class);
$this->expectExceptionMessage('Aborted.');
- $dialog = new QuestionHelper();
- $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new ChoiceQuestion('Choice', ['a', 'b']));
+ (new QuestionHelper())->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new ChoiceQuestion('Choice', ['a', 'b']));
}
public function testAskThrowsExceptionOnMissingInputWithValidator()
{
- $this->expectException(MissingInputException::class);
- $this->expectExceptionMessage('Aborted.');
$dialog = new QuestionHelper();
$question = new Question('What\'s your name?');
@@ -773,6 +765,9 @@ public function testAskThrowsExceptionOnMissingInputWithValidator()
}
});
+ $this->expectException(MissingInputException::class);
+ $this->expectExceptionMessage('Aborted.');
+
$dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), $question);
}
@@ -872,7 +867,6 @@ public function testDisableStty()
$dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question);
} finally {
$reflection = new \ReflectionProperty(QuestionHelper::class, 'stty');
- $reflection->setAccessible(true);
$reflection->setValue(null, true);
}
}
@@ -962,7 +956,7 @@ protected function createInputInterfaceMock($interactive = true)
class AutocompleteValues implements \IteratorAggregate
{
- private $values;
+ private array $values;
public function __construct(array $values)
{
diff --git a/Tests/Helper/SymfonyQuestionHelperTest.php b/Tests/Helper/SymfonyQuestionHelperTest.php
index c6a5bf88a..6cf79965b 100644
--- a/Tests/Helper/SymfonyQuestionHelperTest.php
+++ b/Tests/Helper/SymfonyQuestionHelperTest.php
@@ -101,7 +101,7 @@ public function testAskReturnsNullIfValidatorAllowsIt()
{
$questionHelper = new SymfonyQuestionHelper();
$question = new Question('What is your favorite superhero?');
- $question->setValidator(function ($value) { return $value; });
+ $question->setValidator(fn ($value) => $value);
$input = $this->createStreamableInputInterfaceMock($this->getInputStream("\n"));
$this->assertNull($questionHelper->ask($input, $this->createOutputInterface(), $question));
}
@@ -137,8 +137,7 @@ public function testAskThrowsExceptionOnMissingInput()
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Aborted.');
- $dialog = new SymfonyQuestionHelper();
- $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new Question('What\'s your name?'));
+ (new SymfonyQuestionHelper())->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new Question('What\'s your name?'));
}
public function testChoiceQuestionPadding()
diff --git a/Tests/Helper/TableCellStyleTest.php b/Tests/Helper/TableCellStyleTest.php
index ac80750eb..d934cf801 100644
--- a/Tests/Helper/TableCellStyleTest.php
+++ b/Tests/Helper/TableCellStyleTest.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Console\Tests\Helper;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Helper\TableCellStyle;
class TableCellStyleTest extends TestCase
@@ -21,7 +22,8 @@ public function testCreateTableCellStyle()
$tableCellStyle = new TableCellStyle(['fg' => 'red']);
$this->assertEquals('red', $tableCellStyle->getOptions()['fg']);
- $this->expectException(\Symfony\Component\Console\Exception\InvalidArgumentException::class);
+ $this->expectException(InvalidArgumentException::class);
+
new TableCellStyle(['wrong_key' => null]);
}
}
diff --git a/Tests/Helper/TableStyleTest.php b/Tests/Helper/TableStyleTest.php
index 5ff28f19f..dd740421f 100644
--- a/Tests/Helper/TableStyleTest.php
+++ b/Tests/Helper/TableStyleTest.php
@@ -20,7 +20,6 @@ public function testSetPadTypeWithInvalidType()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).');
- $style = new TableStyle();
- $style->setPadType(31);
+ (new TableStyle())->setPadType(31);
}
}
diff --git a/Tests/Helper/TableTest.php b/Tests/Helper/TableTest.php
index b41c65a2c..608d23c21 100644
--- a/Tests/Helper/TableTest.php
+++ b/Tests/Helper/TableTest.php
@@ -25,6 +25,7 @@
class TableTest extends TestCase
{
+ /** @var resource */
protected $stream;
protected function setUp(): void
@@ -34,8 +35,7 @@ protected function setUp(): void
protected function tearDown(): void
{
- fclose($this->stream);
- $this->stream = null;
+ unset($this->stream);
}
/**
@@ -1017,16 +1017,15 @@ public function testColumnStyle()
public function testThrowsWhenTheCellInAnArray()
{
- $table = new Table($this->getOutputStream());
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('A cell must be a TableCell, a scalar or an object implementing "__toString()", "array" given.');
+ $table = new Table($output = $this->getOutputStream());
$table
->setHeaders(['ISBN', 'Title', 'Author', 'Price'])
->setRows([
['99921-58-10-7', [], 'Dante Alighieri', '9.95'],
]);
- $this->expectException(InvalidArgumentException::class);
- $this->expectExceptionMessage('A cell must be a TableCell, a scalar or an object implementing "__toString()", "array" given.');
-
$table->render();
}
@@ -1603,6 +1602,398 @@ public function testWithColspanAndMaxWith()
$this->assertSame($expected, $this->getOutputContent($output));
}
+ public static function provideRenderVerticalTests(): \Traversable
+ {
+ $books = [
+ ['99921-58-10-7', 'Divine Comedy', 'Dante Alighieri', '9.95'],
+ ['9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens', '139.25'],
+ ];
+
+ yield 'With header for all' => [
+ << [
+ << [
+ << [
+ << [
+ << [
+ << [
+ << [
+ <<99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'],
+ ['9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens>'],
+ ],
+ ];
+
+ yield 'With colspan' => [
+ << 3])],
+ ['9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens'],
+ ],
+ ];
+
+ yield 'With colspans but no header' => [
+ <<consectetur> adipiscing elit, sed> do eiusmod> tempor', ['colspan' => 3])],
+ new TableSeparator(),
+ [new TableCell('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor', ['colspan' => 3])],
+ new TableSeparator(),
+ [new TableCell('Lorem ipsum dolor> sit amet, consectetur ', ['colspan' => 2]), 'hello world'],
+ new TableSeparator(),
+ ['hello world>', new TableCell('Lorem ipsum dolor sit amet, consectetur> adipiscing elit', ['colspan' => 2])],
+ new TableSeparator(),
+ ['hello ', new TableCell('world', ['colspan' => 1]), 'Lorem ipsum dolor sit amet, consectetur'],
+ new TableSeparator(),
+ ['Symfony ', new TableCell('Test', ['colspan' => 1]), 'Lorem ipsum> dolor sit amet, consectetur'],
+ ],
+ ];
+
+ yield 'Borderless style' => [
+ << [
+ << [
+ << [
+ << [
+ << [
+ <<getOutputStream());
+ $table
+ ->setHeaders($headers)
+ ->setRows($rows)
+ ->setVertical()
+ ->setStyle($style);
+
+ if ('' !== $headerTitle) {
+ $table->setHeaderTitle($headerTitle);
+ }
+ if ('' !== $footerTitle) {
+ $table->setFooterTitle($footerTitle);
+ }
+
+ $table->render();
+
+ $this->assertEquals($expectedOutput, $this->getOutputContent($output));
+ }
+
public function testWithHyperlinkAndMaxWidth()
{
$table = new Table($output = $this->getOutputStream(true));
@@ -1628,4 +2019,63 @@ public function testWithHyperlinkAndMaxWidth()
$this->assertSame($expected, $this->getOutputContent($output));
}
+
+ public function testGithubIssue52101HorizontalTrue()
+ {
+ $tableStyle = (new TableStyle())
+ ->setHorizontalBorderChars('─')
+ ->setVerticalBorderChars('│')
+ ->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├')
+ ;
+
+ $table = (new Table($output = $this->getOutputStream()))
+ ->setStyle($tableStyle)
+ ->setHeaderTitle('Title')
+ ->setHeaders(['Hello', 'World'])
+ ->setRows([[1, 2], [3, 4]])
+ ->setHorizontal(true)
+ ;
+ $table->render();
+
+ $this->assertSame(<<getOutputContent($output)
+ );
+ }
+
+ public function testGithubIssue52101HorizontalFalse()
+ {
+ $tableStyle = (new TableStyle())
+ ->setHorizontalBorderChars('─')
+ ->setVerticalBorderChars('│')
+ ->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├')
+ ;
+
+ $table = (new Table($output = $this->getOutputStream()))
+ ->setStyle($tableStyle)
+ ->setHeaderTitle('Title')
+ ->setHeaders(['Hello', 'World'])
+ ->setRows([[1, 2], [3, 4]])
+ ->setHorizontal(false)
+ ;
+ $table->render();
+
+ $this->assertSame(<<getOutputContent($output)
+ );
+ }
}
diff --git a/Tests/Input/ArgvInputTest.php b/Tests/Input/ArgvInputTest.php
index b5289fa01..0e76f9ee6 100644
--- a/Tests/Input/ArgvInputTest.php
+++ b/Tests/Input/ArgvInputTest.php
@@ -25,19 +25,18 @@ public function testConstructor()
$input = new ArgvInput();
$r = new \ReflectionObject($input);
$p = $r->getProperty('tokens');
- $p->setAccessible(true);
- $this->assertEquals(['foo'], $p->getValue($input), '__construct() automatically get its input from the argv server variable');
+ $this->assertSame(['foo'], $p->getValue($input), '__construct() automatically get its input from the argv server variable');
}
public function testParseArguments()
{
$input = new ArgvInput(['cli.php', 'foo']);
$input->bind(new InputDefinition([new InputArgument('name')]));
- $this->assertEquals(['name' => 'foo'], $input->getArguments(), '->parse() parses required arguments');
+ $this->assertSame(['name' => 'foo'], $input->getArguments(), '->parse() parses required arguments');
$input->bind(new InputDefinition([new InputArgument('name')]));
- $this->assertEquals(['name' => 'foo'], $input->getArguments(), '->parse() is stateless');
+ $this->assertSame(['name' => 'foo'], $input->getArguments(), '->parse() is stateless');
}
/**
@@ -58,7 +57,7 @@ public function testParseOptionsNegatable($input, $options, $expectedOptions, $m
{
$input = new ArgvInput($input);
$input->bind(new InputDefinition($options));
- $this->assertEquals($expectedOptions, $input->getOptions(), $message);
+ $this->assertSame($expectedOptions, $input->getOptions(), $message);
}
public static function provideOptions()
@@ -243,8 +242,7 @@ public function testInvalidInput($argv, $definition, $expectedExceptionMessage)
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage($expectedExceptionMessage);
- $input = new ArgvInput($argv);
- $input->bind($definition);
+ (new ArgvInput($argv))->bind($definition);
}
/**
@@ -255,11 +253,10 @@ public function testInvalidInputNegatable($argv, $definition, $expectedException
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage($expectedExceptionMessage);
- $input = new ArgvInput($argv);
- $input->bind($definition);
+ (new ArgvInput($argv))->bind($definition);
}
- public static function provideInvalidInput()
+ public static function provideInvalidInput(): array
{
return [
[
@@ -327,10 +324,15 @@ public static function provideInvalidInput()
new InputDefinition([new InputArgument('name', InputArgument::REQUIRED)]),
'Too many arguments, expected arguments "name".',
],
+ [
+ ['cli.php', ['array']],
+ new InputDefinition(),
+ 'Argument values expected to be all scalars, got "array".',
+ ],
];
}
- public static function provideInvalidNegatableInput()
+ public static function provideInvalidNegatableInput(): array
{
return [
[
@@ -361,7 +363,7 @@ public function testParseArrayArgument()
$input = new ArgvInput(['cli.php', 'foo', 'bar', 'baz', 'bat']);
$input->bind(new InputDefinition([new InputArgument('name', InputArgument::IS_ARRAY)]));
- $this->assertEquals(['name' => ['foo', 'bar', 'baz', 'bat']], $input->getArguments(), '->parse() parses array arguments');
+ $this->assertSame(['name' => ['foo', 'bar', 'baz', 'bat']], $input->getArguments(), '->parse() parses array arguments');
}
public function testParseArrayOption()
@@ -369,11 +371,11 @@ public function testParseArrayOption()
$input = new ArgvInput(['cli.php', '--name=foo', '--name=bar', '--name=baz']);
$input->bind(new InputDefinition([new InputOption('name', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY)]));
- $this->assertEquals(['name' => ['foo', 'bar', 'baz']], $input->getOptions(), '->parse() parses array options ("--option=value" syntax)');
+ $this->assertSame(['name' => ['foo', 'bar', 'baz']], $input->getOptions(), '->parse() parses array options ("--option=value" syntax)');
$input = new ArgvInput(['cli.php', '--name', 'foo', '--name', 'bar', '--name', 'baz']);
$input->bind(new InputDefinition([new InputOption('name', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY)]));
- $this->assertEquals(['name' => ['foo', 'bar', 'baz']], $input->getOptions(), '->parse() parses array options ("--option value" syntax)');
+ $this->assertSame(['name' => ['foo', 'bar', 'baz']], $input->getOptions(), '->parse() parses array options ("--option value" syntax)');
$input = new ArgvInput(['cli.php', '--name=foo', '--name=bar', '--name=']);
$input->bind(new InputDefinition([new InputOption('name', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY)]));
@@ -391,12 +393,12 @@ public function testParseNegativeNumberAfterDoubleDash()
{
$input = new ArgvInput(['cli.php', '--', '-1']);
$input->bind(new InputDefinition([new InputArgument('number')]));
- $this->assertEquals(['number' => '-1'], $input->getArguments(), '->parse() parses arguments with leading dashes as arguments after having encountered a double-dash sequence');
+ $this->assertSame(['number' => '-1'], $input->getArguments(), '->parse() parses arguments with leading dashes as arguments after having encountered a double-dash sequence');
$input = new ArgvInput(['cli.php', '-f', 'bar', '--', '-1']);
$input->bind(new InputDefinition([new InputArgument('number'), new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL)]));
- $this->assertEquals(['foo' => 'bar'], $input->getOptions(), '->parse() parses arguments with leading dashes as options before having encountered a double-dash sequence');
- $this->assertEquals(['number' => '-1'], $input->getArguments(), '->parse() parses arguments with leading dashes as arguments after having encountered a double-dash sequence');
+ $this->assertSame(['foo' => 'bar'], $input->getOptions(), '->parse() parses arguments with leading dashes as options before having encountered a double-dash sequence');
+ $this->assertSame(['number' => '-1'], $input->getArguments(), '->parse() parses arguments with leading dashes as arguments after having encountered a double-dash sequence');
}
public function testParseEmptyStringArgument()
@@ -404,7 +406,7 @@ public function testParseEmptyStringArgument()
$input = new ArgvInput(['cli.php', '-f', 'bar', '']);
$input->bind(new InputDefinition([new InputArgument('empty'), new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL)]));
- $this->assertEquals(['empty' => ''], $input->getArguments(), '->parse() parses empty string arguments');
+ $this->assertSame(['empty' => ''], $input->getArguments(), '->parse() parses empty string arguments');
}
public function testGetFirstArgument()
@@ -413,7 +415,7 @@ public function testGetFirstArgument()
$this->assertNull($input->getFirstArgument(), '->getFirstArgument() returns null when there is no arguments');
$input = new ArgvInput(['cli.php', '-fbbar', 'foo']);
- $this->assertEquals('foo', $input->getFirstArgument(), '->getFirstArgument() returns the first argument from the raw input');
+ $this->assertSame('foo', $input->getFirstArgument(), '->getFirstArgument() returns the first argument from the raw input');
$input = new ArgvInput(['cli.php', '--foo', 'fooval', 'bar']);
$input->bind(new InputDefinition([new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL), new InputArgument('arg')]));
@@ -493,7 +495,7 @@ public function testNoWarningOnInvalidParameterOption()
// No warning thrown
$this->assertFalse($input->hasParameterOption(['-m', '']));
- $this->assertEquals('dev', $input->getParameterOption(['-e', '']));
+ $this->assertSame('dev', $input->getParameterOption(['-e', '']));
// No warning thrown
$this->assertFalse($input->getParameterOption(['-m', '']));
}
@@ -501,10 +503,10 @@ public function testNoWarningOnInvalidParameterOption()
public function testToString()
{
$input = new ArgvInput(['cli.php', '-f', 'foo']);
- $this->assertEquals('-f foo', (string) $input);
+ $this->assertSame('-f foo', (string) $input);
$input = new ArgvInput(['cli.php', '-f', '--bar=foo', 'a b c d', "A\nB'C"]);
- $this->assertEquals('-f --bar=foo '.escapeshellarg('a b c d').' '.escapeshellarg("A\nB'C"), (string) $input);
+ $this->assertSame('-f --bar=foo '.escapeshellarg('a b c d').' '.escapeshellarg("A\nB'C"), (string) $input);
}
/**
@@ -513,7 +515,7 @@ public function testToString()
public function testGetParameterOptionEqualSign($argv, $key, $default, $onlyParams, $expected)
{
$input = new ArgvInput($argv);
- $this->assertEquals($expected, $input->getParameterOption($key, $default, $onlyParams), '->getParameterOption() returns the expected value');
+ $this->assertSame($expected, $input->getParameterOption($key, $default, $onlyParams), '->getParameterOption() returns the expected value');
}
public static function provideGetParameterOptionValues()
@@ -537,32 +539,59 @@ public function testParseSingleDashAsArgument()
{
$input = new ArgvInput(['cli.php', '-']);
$input->bind(new InputDefinition([new InputArgument('file')]));
- $this->assertEquals(['file' => '-'], $input->getArguments(), '->parse() parses single dash as an argument');
+ $this->assertSame(['file' => '-'], $input->getArguments(), '->parse() parses single dash as an argument');
}
public function testParseOptionWithValueOptionalGivenEmptyAndRequiredArgument()
{
$input = new ArgvInput(['cli.php', '--foo=', 'bar']);
$input->bind(new InputDefinition([new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL), new InputArgument('name', InputArgument::REQUIRED)]));
- $this->assertEquals(['foo' => null], $input->getOptions(), '->parse() parses optional options with empty value as null');
- $this->assertEquals(['name' => 'bar'], $input->getArguments(), '->parse() parses required arguments');
+ $this->assertSame(['foo' => ''], $input->getOptions(), '->parse() parses optional options with empty value as null');
+ $this->assertSame(['name' => 'bar'], $input->getArguments(), '->parse() parses required arguments');
$input = new ArgvInput(['cli.php', '--foo=0', 'bar']);
$input->bind(new InputDefinition([new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL), new InputArgument('name', InputArgument::REQUIRED)]));
- $this->assertEquals(['foo' => '0'], $input->getOptions(), '->parse() parses optional options with empty value as null');
- $this->assertEquals(['name' => 'bar'], $input->getArguments(), '->parse() parses required arguments');
+ $this->assertSame(['foo' => '0'], $input->getOptions(), '->parse() parses optional options with empty value as null');
+ $this->assertSame(['name' => 'bar'], $input->getArguments(), '->parse() parses required arguments');
}
public function testParseOptionWithValueOptionalGivenEmptyAndOptionalArgument()
{
$input = new ArgvInput(['cli.php', '--foo=', 'bar']);
$input->bind(new InputDefinition([new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL), new InputArgument('name', InputArgument::OPTIONAL)]));
- $this->assertEquals(['foo' => null], $input->getOptions(), '->parse() parses optional options with empty value as null');
- $this->assertEquals(['name' => 'bar'], $input->getArguments(), '->parse() parses optional arguments');
+ $this->assertSame(['foo' => ''], $input->getOptions(), '->parse() parses optional options with empty value as null');
+ $this->assertSame(['name' => 'bar'], $input->getArguments(), '->parse() parses optional arguments');
$input = new ArgvInput(['cli.php', '--foo=0', 'bar']);
$input->bind(new InputDefinition([new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL), new InputArgument('name', InputArgument::OPTIONAL)]));
- $this->assertEquals(['foo' => '0'], $input->getOptions(), '->parse() parses optional options with empty value as null');
- $this->assertEquals(['name' => 'bar'], $input->getArguments(), '->parse() parses optional arguments');
+ $this->assertSame(['foo' => '0'], $input->getOptions(), '->parse() parses optional options with empty value as null');
+ $this->assertSame(['name' => 'bar'], $input->getArguments(), '->parse() parses optional arguments');
+ }
+
+ public function testGetRawTokensFalse()
+ {
+ $input = new ArgvInput(['cli.php', '--foo', 'bar']);
+ $this->assertSame(['--foo', 'bar'], $input->getRawTokens());
+ }
+
+ /**
+ * @dataProvider provideGetRawTokensTrueTests
+ */
+ public function testGetRawTokensTrue(array $argv, array $expected)
+ {
+ $input = new ArgvInput($argv);
+ $this->assertSame($expected, $input->getRawTokens(true));
+ }
+
+ public static function provideGetRawTokensTrueTests(): iterable
+ {
+ yield [['app/console', 'foo:bar'], []];
+ yield [['app/console', 'foo:bar', '--env=prod'], ['--env=prod']];
+ yield [['app/console', 'foo:bar', '--env', 'prod'], ['--env', 'prod']];
+ yield [['app/console', '--no-ansi', 'foo:bar', '--env', 'prod'], ['--env', 'prod']];
+ yield [['app/console', '--no-ansi', 'foo:bar', '--env', 'prod'], ['--env', 'prod']];
+ yield [['app/console', '--no-ansi', 'foo:bar', 'argument'], ['argument']];
+ yield [['app/console', '--no-ansi', 'foo:bar', 'foo:bar'], ['foo:bar']];
+ yield [['app/console', '--no-ansi', 'foo:bar', '--', 'argument'], ['--', 'argument']];
}
}
diff --git a/Tests/Input/ArrayInputTest.php b/Tests/Input/ArrayInputTest.php
index 733322490..74d2c089f 100644
--- a/Tests/Input/ArrayInputTest.php
+++ b/Tests/Input/ArrayInputTest.php
@@ -24,9 +24,9 @@ public function testGetFirstArgument()
$input = new ArrayInput([]);
$this->assertNull($input->getFirstArgument(), '->getFirstArgument() returns null if no argument were passed');
$input = new ArrayInput(['name' => 'Fabien']);
- $this->assertEquals('Fabien', $input->getFirstArgument(), '->getFirstArgument() returns the first passed argument');
+ $this->assertSame('Fabien', $input->getFirstArgument(), '->getFirstArgument() returns the first passed argument');
$input = new ArrayInput(['--foo' => 'bar', 'name' => 'Fabien']);
- $this->assertEquals('Fabien', $input->getFirstArgument(), '->getFirstArgument() returns the first passed argument');
+ $this->assertSame('Fabien', $input->getFirstArgument(), '->getFirstArgument() returns the first passed argument');
}
public function testHasParameterOption()
@@ -46,22 +46,22 @@ public function testHasParameterOption()
public function testGetParameterOption()
{
$input = new ArrayInput(['name' => 'Fabien', '--foo' => 'bar']);
- $this->assertEquals('bar', $input->getParameterOption('--foo'), '->getParameterOption() returns the option of specified name');
- $this->assertEquals('default', $input->getParameterOption('--bar', 'default'), '->getParameterOption() returns the default value if an option is not present in the passed parameters');
+ $this->assertSame('bar', $input->getParameterOption('--foo'), '->getParameterOption() returns the option of specified name');
+ $this->assertSame('default', $input->getParameterOption('--bar', 'default'), '->getParameterOption() returns the default value if an option is not present in the passed parameters');
$input = new ArrayInput(['Fabien', '--foo' => 'bar']);
- $this->assertEquals('bar', $input->getParameterOption('--foo'), '->getParameterOption() returns the option of specified name');
+ $this->assertSame('bar', $input->getParameterOption('--foo'), '->getParameterOption() returns the option of specified name');
$input = new ArrayInput(['--foo', '--', '--bar' => 'woop']);
- $this->assertEquals('woop', $input->getParameterOption('--bar'), '->getParameterOption() returns the correct value if an option is present in the passed parameters');
- $this->assertEquals('default', $input->getParameterOption('--bar', 'default', true), '->getParameterOption() returns the default value if an option is present in the passed parameters after an end of options signal');
+ $this->assertSame('woop', $input->getParameterOption('--bar'), '->getParameterOption() returns the correct value if an option is present in the passed parameters');
+ $this->assertSame('default', $input->getParameterOption('--bar', 'default', true), '->getParameterOption() returns the default value if an option is present in the passed parameters after an end of options signal');
}
public function testParseArguments()
{
$input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name')]));
- $this->assertEquals(['name' => 'foo'], $input->getArguments(), '->parse() parses required arguments');
+ $this->assertSame(['name' => 'foo'], $input->getArguments(), '->parse() parses required arguments');
}
/**
@@ -71,10 +71,10 @@ public function testParseOptions($input, $options, $expectedOptions, $message)
{
$input = new ArrayInput($input, new InputDefinition($options));
- $this->assertEquals($expectedOptions, $input->getOptions(), $message);
+ $this->assertSame($expectedOptions, $input->getOptions(), $message);
}
- public static function provideOptions()
+ public static function provideOptions(): array
{
return [
[
@@ -133,7 +133,7 @@ public function testParseInvalidInput($parameters, $definition, $expectedExcepti
new ArrayInput($parameters, $definition);
}
- public static function provideInvalidInput()
+ public static function provideInvalidInput(): array
{
return [
[
@@ -162,7 +162,7 @@ public static function provideInvalidInput()
public function testToString()
{
$input = new ArrayInput(['-f' => null, '-b' => 'bar', '--foo' => 'b a z', '--lala' => null, 'test' => 'Foo', 'test2' => "A\nB'C"]);
- $this->assertEquals('-f -b bar --foo='.escapeshellarg('b a z').' --lala Foo '.escapeshellarg("A\nB'C"), (string) $input);
+ $this->assertSame('-f -b bar --foo='.escapeshellarg('b a z').' --lala Foo '.escapeshellarg("A\nB'C"), (string) $input);
$input = new ArrayInput(['-b' => ['bval_1', 'bval_2'], '--f' => ['fval_1', 'fval_2']]);
$this->assertSame('-b bval_1 -b bval_2 --f=fval_1 --f=fval_2', (string) $input);
diff --git a/Tests/Input/InputArgumentTest.php b/Tests/Input/InputArgumentTest.php
index e3cf92467..a9d612f97 100644
--- a/Tests/Input/InputArgumentTest.php
+++ b/Tests/Input/InputArgumentTest.php
@@ -12,6 +12,10 @@
namespace Symfony\Component\Console\Tests\Input;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Completion\CompletionInput;
+use Symfony\Component\Console\Completion\CompletionSuggestions;
+use Symfony\Component\Console\Completion\Suggestion;
+use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\InputArgument;
class InputArgumentTest extends TestCase
@@ -19,7 +23,7 @@ class InputArgumentTest extends TestCase
public function testConstructor()
{
$argument = new InputArgument('foo');
- $this->assertEquals('foo', $argument->getName(), '__construct() takes a name as its first argument');
+ $this->assertSame('foo', $argument->getName(), '__construct() takes a name as its first argument');
}
public function testModes()
@@ -58,13 +62,13 @@ public function testIsArray()
public function testGetDescription()
{
$argument = new InputArgument('foo', null, 'Some description');
- $this->assertEquals('Some description', $argument->getDescription(), '->getDescription() return the message description');
+ $this->assertSame('Some description', $argument->getDescription(), '->getDescription() return the message description');
}
public function testGetDefault()
{
$argument = new InputArgument('foo', InputArgument::OPTIONAL, '', 'default');
- $this->assertEquals('default', $argument->getDefault(), '->getDefault() return the default value');
+ $this->assertSame('default', $argument->getDefault(), '->getDefault() return the default value');
}
public function testSetDefault()
@@ -73,34 +77,70 @@ public function testSetDefault()
$argument->setDefault(null);
$this->assertNull($argument->getDefault(), '->setDefault() can reset the default value by passing null');
$argument->setDefault('another');
- $this->assertEquals('another', $argument->getDefault(), '->setDefault() changes the default value');
+ $this->assertSame('another', $argument->getDefault(), '->setDefault() changes the default value');
$argument = new InputArgument('foo', InputArgument::OPTIONAL | InputArgument::IS_ARRAY);
$argument->setDefault([1, 2]);
- $this->assertEquals([1, 2], $argument->getDefault(), '->setDefault() changes the default value');
+ $this->assertSame([1, 2], $argument->getDefault(), '->setDefault() changes the default value');
}
public function testSetDefaultWithRequiredArgument()
{
+ $argument = new InputArgument('foo', InputArgument::REQUIRED);
+
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot set a default value except for InputArgument::OPTIONAL mode.');
- $argument = new InputArgument('foo', InputArgument::REQUIRED);
+
$argument->setDefault('default');
}
public function testSetDefaultWithRequiredArrayArgument()
{
+ $argument = new InputArgument('foo', InputArgument::REQUIRED | InputArgument::IS_ARRAY);
+
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot set a default value except for InputArgument::OPTIONAL mode.');
- $argument = new InputArgument('foo', InputArgument::REQUIRED | InputArgument::IS_ARRAY);
+
$argument->setDefault([]);
}
public function testSetDefaultWithArrayArgument()
{
+ $argument = new InputArgument('foo', InputArgument::IS_ARRAY);
+
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('A default value for an array argument must be an array.');
- $argument = new InputArgument('foo', InputArgument::IS_ARRAY);
+
$argument->setDefault('default');
}
+
+ public function testCompleteArray()
+ {
+ $values = ['foo', 'bar'];
+ $argument = new InputArgument('foo', null, '', null, $values);
+ $this->assertTrue($argument->hasCompletion());
+ $suggestions = new CompletionSuggestions();
+ $argument->complete(new CompletionInput(), $suggestions);
+ $this->assertSame($values, array_map(fn (Suggestion $suggestion) => $suggestion->getValue(), $suggestions->getValueSuggestions()));
+ }
+
+ public function testCompleteClosure()
+ {
+ $values = ['foo', 'bar'];
+ $argument = new InputArgument('foo', null, '', null, fn (CompletionInput $input): array => $values);
+ $this->assertTrue($argument->hasCompletion());
+ $suggestions = new CompletionSuggestions();
+ $argument->complete(new CompletionInput(), $suggestions);
+ $this->assertSame($values, array_map(fn (Suggestion $suggestion) => $suggestion->getValue(), $suggestions->getValueSuggestions()));
+ }
+
+ public function testCompleteClosureReturnIncorrectType()
+ {
+ $argument = new InputArgument('foo', InputArgument::OPTIONAL, '', null, fn (CompletionInput $input) => 'invalid');
+
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Closure for argument "foo" must return an array. Got "string".');
+
+ $argument->complete(new CompletionInput(), new CompletionSuggestions());
+ }
}
diff --git a/Tests/Input/InputDefinitionTest.php b/Tests/Input/InputDefinitionTest.php
index 39157af8f..ab203e6e5 100644
--- a/Tests/Input/InputDefinitionTest.php
+++ b/Tests/Input/InputDefinitionTest.php
@@ -18,7 +18,7 @@
class InputDefinitionTest extends TestCase
{
- protected static $fixtures;
+ protected static string $fixtures;
protected $multi;
protected $foo;
@@ -36,10 +36,10 @@ public function testConstructorArguments()
$this->initializeArguments();
$definition = new InputDefinition();
- $this->assertEquals([], $definition->getArguments(), '__construct() creates a new InputDefinition object');
+ $this->assertSame([], $definition->getArguments(), '__construct() creates a new InputDefinition object');
$definition = new InputDefinition([$this->foo, $this->bar]);
- $this->assertEquals(['foo' => $this->foo, 'bar' => $this->bar], $definition->getArguments(), '__construct() takes an array of InputArgument objects as its first argument');
+ $this->assertSame(['foo' => $this->foo, 'bar' => $this->bar], $definition->getArguments(), '__construct() takes an array of InputArgument objects as its first argument');
}
public function testConstructorOptions()
@@ -47,10 +47,10 @@ public function testConstructorOptions()
$this->initializeOptions();
$definition = new InputDefinition();
- $this->assertEquals([], $definition->getOptions(), '__construct() creates a new InputDefinition object');
+ $this->assertSame([], $definition->getOptions(), '__construct() creates a new InputDefinition object');
$definition = new InputDefinition([$this->foo, $this->bar]);
- $this->assertEquals(['foo' => $this->foo, 'bar' => $this->bar], $definition->getOptions(), '__construct() takes an array of InputOption objects as its first argument');
+ $this->assertSame(['foo' => $this->foo, 'bar' => $this->bar], $definition->getOptions(), '__construct() takes an array of InputOption objects as its first argument');
}
public function testSetArguments()
@@ -59,10 +59,10 @@ public function testSetArguments()
$definition = new InputDefinition();
$definition->setArguments([$this->foo]);
- $this->assertEquals(['foo' => $this->foo], $definition->getArguments(), '->setArguments() sets the array of InputArgument objects');
+ $this->assertSame(['foo' => $this->foo], $definition->getArguments(), '->setArguments() sets the array of InputArgument objects');
$definition->setArguments([$this->bar]);
- $this->assertEquals(['bar' => $this->bar], $definition->getArguments(), '->setArguments() clears all InputArgument objects');
+ $this->assertSame(['bar' => $this->bar], $definition->getArguments(), '->setArguments() clears all InputArgument objects');
}
public function testAddArguments()
@@ -71,9 +71,9 @@ public function testAddArguments()
$definition = new InputDefinition();
$definition->addArguments([$this->foo]);
- $this->assertEquals(['foo' => $this->foo], $definition->getArguments(), '->addArguments() adds an array of InputArgument objects');
+ $this->assertSame(['foo' => $this->foo], $definition->getArguments(), '->addArguments() adds an array of InputArgument objects');
$definition->addArguments([$this->bar]);
- $this->assertEquals(['foo' => $this->foo, 'bar' => $this->bar], $definition->getArguments(), '->addArguments() does not clear existing InputArgument objects');
+ $this->assertSame(['foo' => $this->foo, 'bar' => $this->bar], $definition->getArguments(), '->addArguments() does not clear existing InputArgument objects');
}
public function testAddArgument()
@@ -82,9 +82,9 @@ public function testAddArgument()
$definition = new InputDefinition();
$definition->addArgument($this->foo);
- $this->assertEquals(['foo' => $this->foo], $definition->getArguments(), '->addArgument() adds a InputArgument object');
+ $this->assertSame(['foo' => $this->foo], $definition->getArguments(), '->addArgument() adds a InputArgument object');
$definition->addArgument($this->bar);
- $this->assertEquals(['foo' => $this->foo, 'bar' => $this->bar], $definition->getArguments(), '->addArgument() adds a InputArgument object');
+ $this->assertSame(['foo' => $this->foo, 'bar' => $this->bar], $definition->getArguments(), '->addArgument() adds a InputArgument object');
}
public function testArgumentsMustHaveDifferentNames()
@@ -113,7 +113,7 @@ public function testRequiredArgumentCannotFollowAnOptionalOne()
{
$this->initializeArguments();
$this->expectException(\LogicException::class);
- $this->expectExceptionMessage(sprintf('Cannot add a required argument "%s" after an optional one "%s".', $this->foo2->getName(), $this->foo->getName()));
+ $this->expectExceptionMessage(\sprintf('Cannot add a required argument "%s" after an optional one "%s".', $this->foo2->getName(), $this->foo->getName()));
$definition = new InputDefinition();
$definition->addArgument($this->foo);
@@ -126,7 +126,7 @@ public function testGetArgument()
$definition = new InputDefinition();
$definition->addArguments([$this->foo]);
- $this->assertEquals($this->foo, $definition->getArgument('foo'), '->getArgument() returns a InputArgument by its name');
+ $this->assertSame($this->foo, $definition->getArgument('foo'), '->getArgument() returns a InputArgument by its name');
}
public function testGetInvalidArgument()
@@ -157,9 +157,9 @@ public function testGetArgumentRequiredCount()
$definition = new InputDefinition();
$definition->addArgument($this->foo2);
- $this->assertEquals(1, $definition->getArgumentRequiredCount(), '->getArgumentRequiredCount() returns the number of required arguments');
+ $this->assertSame(1, $definition->getArgumentRequiredCount(), '->getArgumentRequiredCount() returns the number of required arguments');
$definition->addArgument($this->foo);
- $this->assertEquals(1, $definition->getArgumentRequiredCount(), '->getArgumentRequiredCount() returns the number of required arguments');
+ $this->assertSame(1, $definition->getArgumentRequiredCount(), '->getArgumentRequiredCount() returns the number of required arguments');
}
public function testGetArgumentCount()
@@ -168,9 +168,9 @@ public function testGetArgumentCount()
$definition = new InputDefinition();
$definition->addArgument($this->foo2);
- $this->assertEquals(1, $definition->getArgumentCount(), '->getArgumentCount() returns the number of arguments');
+ $this->assertSame(1, $definition->getArgumentCount(), '->getArgumentCount() returns the number of arguments');
$definition->addArgument($this->foo);
- $this->assertEquals(2, $definition->getArgumentCount(), '->getArgumentCount() returns the number of arguments');
+ $this->assertSame(2, $definition->getArgumentCount(), '->getArgumentCount() returns the number of arguments');
}
public function testGetArgumentDefaults()
@@ -179,14 +179,14 @@ public function testGetArgumentDefaults()
new InputArgument('foo1', InputArgument::OPTIONAL),
new InputArgument('foo2', InputArgument::OPTIONAL, '', 'default'),
new InputArgument('foo3', InputArgument::OPTIONAL | InputArgument::IS_ARRAY),
- // new InputArgument('foo4', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, '', [1, 2]),
+ // new InputArgument('foo4', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, '', [1, 2]),
]);
- $this->assertEquals(['foo1' => null, 'foo2' => 'default', 'foo3' => []], $definition->getArgumentDefaults(), '->getArgumentDefaults() return the default values for each argument');
+ $this->assertSame(['foo1' => null, 'foo2' => 'default', 'foo3' => []], $definition->getArgumentDefaults(), '->getArgumentDefaults() return the default values for each argument');
$definition = new InputDefinition([
new InputArgument('foo4', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, '', [1, 2]),
]);
- $this->assertEquals(['foo4' => [1, 2]], $definition->getArgumentDefaults(), '->getArgumentDefaults() return the default values for each argument');
+ $this->assertSame(['foo4' => [1, 2]], $definition->getArgumentDefaults(), '->getArgumentDefaults() return the default values for each argument');
}
public function testSetOptions()
@@ -194,9 +194,9 @@ public function testSetOptions()
$this->initializeOptions();
$definition = new InputDefinition([$this->foo]);
- $this->assertEquals(['foo' => $this->foo], $definition->getOptions(), '->setOptions() sets the array of InputOption objects');
+ $this->assertSame(['foo' => $this->foo], $definition->getOptions(), '->setOptions() sets the array of InputOption objects');
$definition->setOptions([$this->bar]);
- $this->assertEquals(['bar' => $this->bar], $definition->getOptions(), '->setOptions() clears all InputOption objects');
+ $this->assertSame(['bar' => $this->bar], $definition->getOptions(), '->setOptions() clears all InputOption objects');
}
public function testSetOptionsClearsOptions()
@@ -215,9 +215,9 @@ public function testAddOptions()
$this->initializeOptions();
$definition = new InputDefinition([$this->foo]);
- $this->assertEquals(['foo' => $this->foo], $definition->getOptions(), '->addOptions() adds an array of InputOption objects');
+ $this->assertSame(['foo' => $this->foo], $definition->getOptions(), '->addOptions() adds an array of InputOption objects');
$definition->addOptions([$this->bar]);
- $this->assertEquals(['foo' => $this->foo, 'bar' => $this->bar], $definition->getOptions(), '->addOptions() does not clear existing InputOption objects');
+ $this->assertSame(['foo' => $this->foo, 'bar' => $this->bar], $definition->getOptions(), '->addOptions() does not clear existing InputOption objects');
}
public function testAddOption()
@@ -226,9 +226,9 @@ public function testAddOption()
$definition = new InputDefinition();
$definition->addOption($this->foo);
- $this->assertEquals(['foo' => $this->foo], $definition->getOptions(), '->addOption() adds a InputOption object');
+ $this->assertSame(['foo' => $this->foo], $definition->getOptions(), '->addOption() adds a InputOption object');
$definition->addOption($this->bar);
- $this->assertEquals(['foo' => $this->foo, 'bar' => $this->bar], $definition->getOptions(), '->addOption() adds a InputOption object');
+ $this->assertSame(['foo' => $this->foo, 'bar' => $this->bar], $definition->getOptions(), '->addOption() adds a InputOption object');
}
public function testAddDuplicateOption()
@@ -278,7 +278,7 @@ public function testGetOption()
$this->initializeOptions();
$definition = new InputDefinition([$this->foo]);
- $this->assertEquals($this->foo, $definition->getOption('foo'), '->getOption() returns a InputOption by its name');
+ $this->assertSame($this->foo, $definition->getOption('foo'), '->getOption() returns a InputOption by its name');
}
public function testGetInvalidOption()
@@ -314,7 +314,7 @@ public function testGetOptionForShortcut()
$this->initializeOptions();
$definition = new InputDefinition([$this->foo]);
- $this->assertEquals($this->foo, $definition->getOptionForShortcut('f'), '->getOptionForShortcut() returns a InputOption by its shortcut');
+ $this->assertSame($this->foo, $definition->getOptionForShortcut('f'), '->getOptionForShortcut() returns a InputOption by its shortcut');
}
public function testGetOptionForMultiShortcut()
@@ -322,8 +322,8 @@ public function testGetOptionForMultiShortcut()
$this->initializeOptions();
$definition = new InputDefinition([$this->multi]);
- $this->assertEquals($this->multi, $definition->getOptionForShortcut('m'), '->getOptionForShortcut() returns a InputOption by its shortcut');
- $this->assertEquals($this->multi, $definition->getOptionForShortcut('mmm'), '->getOptionForShortcut() returns a InputOption by its shortcut');
+ $this->assertSame($this->multi, $definition->getOptionForShortcut('m'), '->getOptionForShortcut() returns a InputOption by its shortcut');
+ $this->assertSame($this->multi, $definition->getOptionForShortcut('mmm'), '->getOptionForShortcut() returns a InputOption by its shortcut');
}
public function testGetOptionForInvalidShortcut()
@@ -364,7 +364,7 @@ public function testGetOptionDefaults()
*/
public function testGetSynopsis(InputDefinition $definition, $expectedSynopsis, $message = null)
{
- $this->assertEquals($expectedSynopsis, $definition->getSynopsis(), $message ? '->getSynopsis() '.$message : '');
+ $this->assertSame($expectedSynopsis, $definition->getSynopsis(), $message ? '->getSynopsis() '.$message : '');
}
public static function getGetSynopsisData()
@@ -388,7 +388,7 @@ public static function getGetSynopsisData()
public function testGetShortSynopsis()
{
$definition = new InputDefinition([new InputOption('foo'), new InputOption('bar'), new InputArgument('cat')]);
- $this->assertEquals('[options] [--] []', $definition->getSynopsis(true), '->getSynopsis(true) groups options in [options]');
+ $this->assertSame('[options] [--] []', $definition->getSynopsis(true), '->getSynopsis(true) groups options in [options]');
}
protected function initializeArguments()
diff --git a/Tests/Input/InputOptionTest.php b/Tests/Input/InputOptionTest.php
index 83b295fcc..47ab503f7 100644
--- a/Tests/Input/InputOptionTest.php
+++ b/Tests/Input/InputOptionTest.php
@@ -12,6 +12,10 @@
namespace Symfony\Component\Console\Tests\Input;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Completion\CompletionInput;
+use Symfony\Component\Console\Completion\CompletionSuggestions;
+use Symfony\Component\Console\Completion\Suggestion;
+use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Input\InputOption;
class InputOptionTest extends TestCase
@@ -19,9 +23,9 @@ class InputOptionTest extends TestCase
public function testConstructor()
{
$option = new InputOption('foo');
- $this->assertEquals('foo', $option->getName(), '__construct() takes a name as its first argument');
+ $this->assertSame('foo', $option->getName(), '__construct() takes a name as its first argument');
$option = new InputOption('--foo');
- $this->assertEquals('foo', $option->getName(), '__construct() removes the leading -- of the option name');
+ $this->assertSame('foo', $option->getName(), '__construct() removes the leading -- of the option name');
}
public function testArrayModeWithoutValue()
@@ -48,11 +52,11 @@ public function testBooleanWithOptional()
public function testShortcut()
{
$option = new InputOption('foo', 'f');
- $this->assertEquals('f', $option->getShortcut(), '__construct() can take a shortcut as its second argument');
+ $this->assertSame('f', $option->getShortcut(), '__construct() can take a shortcut as its second argument');
$option = new InputOption('foo', '-f|-ff|fff');
- $this->assertEquals('f|ff|fff', $option->getShortcut(), '__construct() removes the leading - of the shortcuts');
+ $this->assertSame('f|ff|fff', $option->getShortcut(), '__construct() removes the leading - of the shortcuts');
$option = new InputOption('foo', ['f', 'ff', '-fff']);
- $this->assertEquals('f|ff|fff', $option->getShortcut(), '__construct() removes the leading - of the shortcuts');
+ $this->assertSame('f|ff|fff', $option->getShortcut(), '__construct() removes the leading - of the shortcuts');
$option = new InputOption('foo');
$this->assertNull($option->getShortcut(), '__construct() makes the shortcut null by default');
$option = new InputOption('foo', '');
@@ -60,15 +64,15 @@ public function testShortcut()
$option = new InputOption('foo', []);
$this->assertNull($option->getShortcut(), '__construct() makes the shortcut null when given an empty array');
$option = new InputOption('foo', ['f', '', 'fff']);
- $this->assertEquals('f|fff', $option->getShortcut(), '__construct() removes empty shortcuts');
+ $this->assertSame('f|fff', $option->getShortcut(), '__construct() removes empty shortcuts');
$option = new InputOption('foo', 'f||fff');
- $this->assertEquals('f|fff', $option->getShortcut(), '__construct() removes empty shortcuts');
+ $this->assertSame('f|fff', $option->getShortcut(), '__construct() removes empty shortcuts');
$option = new InputOption('foo', '0');
- $this->assertEquals('0', $option->getShortcut(), '-0 is an acceptable shortcut value');
+ $this->assertSame('0', $option->getShortcut(), '-0 is an acceptable shortcut value');
$option = new InputOption('foo', ['0', 'z']);
- $this->assertEquals('0|z', $option->getShortcut(), '-0 is an acceptable shortcut value when embedded in an array');
+ $this->assertSame('0|z', $option->getShortcut(), '-0 is an acceptable shortcut value when embedded in an array');
$option = new InputOption('foo', '0|z');
- $this->assertEquals('0|z', $option->getShortcut(), '-0 is an acceptable shortcut value when embedded in a string-list');
+ $this->assertSame('0|z', $option->getShortcut(), '-0 is an acceptable shortcut value when embedded in a string-list');
$option = new InputOption('foo', false);
$this->assertNull($option->getShortcut(), '__construct() makes the shortcut null when given a false as value');
}
@@ -138,22 +142,22 @@ public function testIsArray()
public function testGetDescription()
{
$option = new InputOption('foo', 'f', null, 'Some description');
- $this->assertEquals('Some description', $option->getDescription(), '->getDescription() returns the description message');
+ $this->assertSame('Some description', $option->getDescription(), '->getDescription() returns the description message');
}
public function testGetDefault()
{
$option = new InputOption('foo', null, InputOption::VALUE_OPTIONAL, '', 'default');
- $this->assertEquals('default', $option->getDefault(), '->getDefault() returns the default value');
+ $this->assertSame('default', $option->getDefault(), '->getDefault() returns the default value');
$option = new InputOption('foo', null, InputOption::VALUE_REQUIRED, '', 'default');
- $this->assertEquals('default', $option->getDefault(), '->getDefault() returns the default value');
+ $this->assertSame('default', $option->getDefault(), '->getDefault() returns the default value');
$option = new InputOption('foo', null, InputOption::VALUE_REQUIRED);
$this->assertNull($option->getDefault(), '->getDefault() returns null if no default value is configured');
$option = new InputOption('foo', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY);
- $this->assertEquals([], $option->getDefault(), '->getDefault() returns an empty array if option is an array');
+ $this->assertSame([], $option->getDefault(), '->getDefault() returns an empty array if option is an array');
$option = new InputOption('foo', null, InputOption::VALUE_NONE);
$this->assertFalse($option->getDefault(), '->getDefault() returns false if the option does not take a value');
@@ -165,26 +169,30 @@ public function testSetDefault()
$option->setDefault(null);
$this->assertNull($option->getDefault(), '->setDefault() can reset the default value by passing null');
$option->setDefault('another');
- $this->assertEquals('another', $option->getDefault(), '->setDefault() changes the default value');
+ $this->assertSame('another', $option->getDefault(), '->setDefault() changes the default value');
$option = new InputOption('foo', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY);
$option->setDefault([1, 2]);
- $this->assertEquals([1, 2], $option->getDefault(), '->setDefault() changes the default value');
+ $this->assertSame([1, 2], $option->getDefault(), '->setDefault() changes the default value');
}
public function testDefaultValueWithValueNoneMode()
{
+ $option = new InputOption('foo', 'f', InputOption::VALUE_NONE);
+
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('Cannot set a default value when using InputOption::VALUE_NONE mode.');
- $option = new InputOption('foo', 'f', InputOption::VALUE_NONE);
+
$option->setDefault('default');
}
public function testDefaultValueWithIsArrayMode()
{
+ $option = new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY);
+
$this->expectException(\LogicException::class);
$this->expectExceptionMessage('A default value for an array option must be an array.');
- $option = new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY);
+
$option->setDefault('default');
}
@@ -210,4 +218,42 @@ public function testEquals()
$option2 = new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL, 'Some description');
$this->assertFalse($option->equals($option2));
}
+
+ public function testSuggestedValuesErrorIfNoValue()
+ {
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Cannot set suggested values if the option does not accept a value.');
+
+ new InputOption('foo', null, InputOption::VALUE_NONE, '', null, ['foo']);
+ }
+
+ public function testCompleteArray()
+ {
+ $values = ['foo', 'bar'];
+ $option = new InputOption('foo', null, InputOption::VALUE_OPTIONAL, '', null, $values);
+ $this->assertTrue($option->hasCompletion());
+ $suggestions = new CompletionSuggestions();
+ $option->complete(new CompletionInput(), $suggestions);
+ $this->assertSame($values, array_map(fn (Suggestion $suggestion) => $suggestion->getValue(), $suggestions->getValueSuggestions()));
+ }
+
+ public function testCompleteClosure()
+ {
+ $values = ['foo', 'bar'];
+ $option = new InputOption('foo', null, InputOption::VALUE_OPTIONAL, '', null, fn (CompletionInput $input): array => $values);
+ $this->assertTrue($option->hasCompletion());
+ $suggestions = new CompletionSuggestions();
+ $option->complete(new CompletionInput(), $suggestions);
+ $this->assertSame($values, array_map(fn (Suggestion $suggestion) => $suggestion->getValue(), $suggestions->getValueSuggestions()));
+ }
+
+ public function testCompleteClosureReturnIncorrectType()
+ {
+ $option = new InputOption('foo', null, InputOption::VALUE_OPTIONAL, '', null, fn (CompletionInput $input) => 'invalid');
+
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage('Closure for option "foo" must return an array. Got "string".');
+
+ $option->complete(new CompletionInput(), new CompletionSuggestions());
+ }
}
diff --git a/Tests/Input/InputTest.php b/Tests/Input/InputTest.php
index 6547822fb..19a840da6 100644
--- a/Tests/Input/InputTest.php
+++ b/Tests/Input/InputTest.php
@@ -22,29 +22,29 @@ class InputTest extends TestCase
public function testConstructor()
{
$input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name')]));
- $this->assertEquals('foo', $input->getArgument('name'), '->__construct() takes a InputDefinition as an argument');
+ $this->assertSame('foo', $input->getArgument('name'), '->__construct() takes a InputDefinition as an argument');
}
public function testOptions()
{
$input = new ArrayInput(['--name' => 'foo'], new InputDefinition([new InputOption('name')]));
- $this->assertEquals('foo', $input->getOption('name'), '->getOption() returns the value for the given option');
+ $this->assertSame('foo', $input->getOption('name'), '->getOption() returns the value for the given option');
$input->setOption('name', 'bar');
- $this->assertEquals('bar', $input->getOption('name'), '->setOption() sets the value for a given option');
- $this->assertEquals(['name' => 'bar'], $input->getOptions(), '->getOptions() returns all option values');
+ $this->assertSame('bar', $input->getOption('name'), '->setOption() sets the value for a given option');
+ $this->assertSame(['name' => 'bar'], $input->getOptions(), '->getOptions() returns all option values');
$input = new ArrayInput(['--name' => 'foo'], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')]));
- $this->assertEquals('default', $input->getOption('bar'), '->getOption() returns the default value for optional options');
- $this->assertEquals(['name' => 'foo', 'bar' => 'default'], $input->getOptions(), '->getOptions() returns all option values, even optional ones');
+ $this->assertSame('default', $input->getOption('bar'), '->getOption() returns the default value for optional options');
+ $this->assertSame(['name' => 'foo', 'bar' => 'default'], $input->getOptions(), '->getOptions() returns all option values, even optional ones');
$input = new ArrayInput(['--name' => 'foo', '--bar' => ''], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')]));
- $this->assertEquals('', $input->getOption('bar'), '->getOption() returns null for options explicitly passed without value (or an empty value)');
- $this->assertEquals(['name' => 'foo', 'bar' => ''], $input->getOptions(), '->getOptions() returns all option values.');
+ $this->assertSame('', $input->getOption('bar'), '->getOption() returns null for options explicitly passed without value (or an empty value)');
+ $this->assertSame(['name' => 'foo', 'bar' => ''], $input->getOptions(), '->getOptions() returns all option values.');
$input = new ArrayInput(['--name' => 'foo', '--bar' => null], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')]));
$this->assertNull($input->getOption('bar'), '->getOption() returns null for options explicitly passed without value (or an empty value)');
- $this->assertEquals(['name' => 'foo', 'bar' => null], $input->getOptions(), '->getOptions() returns all option values');
+ $this->assertSame(['name' => 'foo', 'bar' => null], $input->getOptions(), '->getOptions() returns all option values');
$input = new ArrayInput(['--name' => null], new InputDefinition([new InputOption('name', null, InputOption::VALUE_NEGATABLE)]));
$this->assertTrue($input->hasOption('name'));
@@ -63,65 +63,77 @@ public function testOptions()
public function testSetInvalidOption()
{
+ $input = new ArrayInput(['--name' => 'foo'], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')]));
+
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The "foo" option does not exist.');
- $input = new ArrayInput(['--name' => 'foo'], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')]));
+
$input->setOption('foo', 'bar');
}
public function testGetInvalidOption()
{
+ $input = new ArrayInput(['--name' => 'foo'], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')]));
+
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The "foo" option does not exist.');
- $input = new ArrayInput(['--name' => 'foo'], new InputDefinition([new InputOption('name'), new InputOption('bar', '', InputOption::VALUE_OPTIONAL, '', 'default')]));
+
$input->getOption('foo');
}
public function testArguments()
{
$input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name')]));
- $this->assertEquals('foo', $input->getArgument('name'), '->getArgument() returns the value for the given argument');
+ $this->assertSame('foo', $input->getArgument('name'), '->getArgument() returns the value for the given argument');
$input->setArgument('name', 'bar');
- $this->assertEquals('bar', $input->getArgument('name'), '->setArgument() sets the value for a given argument');
- $this->assertEquals(['name' => 'bar'], $input->getArguments(), '->getArguments() returns all argument values');
+ $this->assertSame('bar', $input->getArgument('name'), '->setArgument() sets the value for a given argument');
+ $this->assertSame(['name' => 'bar'], $input->getArguments(), '->getArguments() returns all argument values');
$input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name'), new InputArgument('bar', InputArgument::OPTIONAL, '', 'default')]));
- $this->assertEquals('default', $input->getArgument('bar'), '->getArgument() returns the default value for optional arguments');
- $this->assertEquals(['name' => 'foo', 'bar' => 'default'], $input->getArguments(), '->getArguments() returns all argument values, even optional ones');
+ $this->assertSame('default', $input->getArgument('bar'), '->getArgument() returns the default value for optional arguments');
+ $this->assertSame(['name' => 'foo', 'bar' => 'default'], $input->getArguments(), '->getArguments() returns all argument values, even optional ones');
}
public function testSetInvalidArgument()
{
+ $input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name'), new InputArgument('bar', InputArgument::OPTIONAL, '', 'default')]));
+
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The "foo" argument does not exist.');
- $input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name'), new InputArgument('bar', InputArgument::OPTIONAL, '', 'default')]));
+
$input->setArgument('foo', 'bar');
}
public function testGetInvalidArgument()
{
+ $input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name'), new InputArgument('bar', InputArgument::OPTIONAL, '', 'default')]));
+
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('The "foo" argument does not exist.');
- $input = new ArrayInput(['name' => 'foo'], new InputDefinition([new InputArgument('name'), new InputArgument('bar', InputArgument::OPTIONAL, '', 'default')]));
+
$input->getArgument('foo');
}
public function testValidateWithMissingArguments()
{
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('Not enough arguments (missing: "name").');
$input = new ArrayInput([]);
$input->bind(new InputDefinition([new InputArgument('name', InputArgument::REQUIRED)]));
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('Not enough arguments (missing: "name").');
+
$input->validate();
}
public function testValidateWithMissingRequiredArguments()
{
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('Not enough arguments (missing: "name").');
$input = new ArrayInput(['bar' => 'baz']);
$input->bind(new InputDefinition([new InputArgument('name', InputArgument::REQUIRED), new InputArgument('bar', InputArgument::OPTIONAL)]));
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('Not enough arguments (missing: "name").');
+
$input->validate();
}
diff --git a/Tests/Input/StringInputTest.php b/Tests/Input/StringInputTest.php
index c55256c03..92425daab 100644
--- a/Tests/Input/StringInputTest.php
+++ b/Tests/Input/StringInputTest.php
@@ -27,8 +27,7 @@ public function testTokenize($input, $tokens, $message)
$input = new StringInput($input);
$r = new \ReflectionClass(ArgvInput::class);
$p = $r->getProperty('tokens');
- $p->setAccessible(true);
- $this->assertEquals($tokens, $p->getValue($input), $message);
+ $this->assertSame($tokens, $p->getValue($input), $message);
}
public function testInputOptionWithGivenString()
@@ -40,7 +39,7 @@ public function testInputOptionWithGivenString()
// call to bind
$input = new StringInput('--foo=bar');
$input->bind($definition);
- $this->assertEquals('bar', $input->getOption('foo'));
+ $this->assertSame('bar', $input->getOption('foo'));
}
public static function getTokenizeData()
@@ -78,12 +77,12 @@ public static function getTokenizeData()
public function testToString()
{
$input = new StringInput('-f foo');
- $this->assertEquals('-f foo', (string) $input);
+ $this->assertSame('-f foo', (string) $input);
$input = new StringInput('-f --bar=foo "a b c d"');
- $this->assertEquals('-f --bar=foo '.escapeshellarg('a b c d'), (string) $input);
+ $this->assertSame('-f --bar=foo '.escapeshellarg('a b c d'), (string) $input);
$input = new StringInput('-f --bar=foo \'a b c d\' '."'A\nB\\'C'");
- $this->assertEquals('-f --bar=foo '.escapeshellarg('a b c d').' '.escapeshellarg("A\nB'C"), (string) $input);
+ $this->assertSame('-f --bar=foo '.escapeshellarg('a b c d').' '.escapeshellarg("A\nB'C"), (string) $input);
}
}
diff --git a/Tests/Logger/ConsoleLoggerTest.php b/Tests/Logger/ConsoleLoggerTest.php
index 4a96ddb15..0464c8c5f 100644
--- a/Tests/Logger/ConsoleLoggerTest.php
+++ b/Tests/Logger/ConsoleLoggerTest.php
@@ -26,10 +26,7 @@
*/
class ConsoleLoggerTest extends TestCase
{
- /**
- * @var DummyOutput
- */
- protected $output;
+ protected DummyOutput $output;
public function getLogger(): LoggerInterface
{
@@ -140,8 +137,7 @@ public static function provideLevelsAndMessages()
public function testThrowsOnInvalidLevel()
{
$this->expectException(InvalidArgumentException::class);
- $logger = $this->getLogger();
- $logger->log('invalid level', 'Foo');
+ $this->getLogger()->log('invalid level', 'Foo');
}
public function testContextReplacement()
@@ -155,11 +151,7 @@ public function testContextReplacement()
public function testObjectCastToString()
{
- if (method_exists($this, 'createPartialMock')) {
- $dummy = $this->createPartialMock(DummyTest::class, ['__toString']);
- } else {
- $dummy = $this->createPartialMock(DummyTest::class, ['__toString']);
- }
+ $dummy = $this->createPartialMock(DummyTest::class, ['__toString']);
$dummy->method('__toString')->willReturn('DUMMY');
$this->getLogger()->warning($dummy);
@@ -177,7 +169,7 @@ public function testContextCanContainAnything()
'int' => 0,
'float' => 0.5,
'nested' => ['with object' => new DummyTest()],
- 'object' => new \DateTime(),
+ 'object' => new \DateTimeImmutable(),
'resource' => fopen('php://memory', 'r'),
];
diff --git a/Tests/Messenger/RunCommandMessageHandlerTest.php b/Tests/Messenger/RunCommandMessageHandlerTest.php
new file mode 100644
index 000000000..58b33d565
--- /dev/null
+++ b/Tests/Messenger/RunCommandMessageHandlerTest.php
@@ -0,0 +1,114 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Tests\Messenger;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Exception\RunCommandFailedException;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Messenger\RunCommandMessage;
+use Symfony\Component\Console\Messenger\RunCommandMessageHandler;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * @author Kevin Bond
+ */
+final class RunCommandMessageHandlerTest extends TestCase
+{
+ public function testExecutesCommand()
+ {
+ $handler = new RunCommandMessageHandler($this->createApplicationWithCommand());
+ $context = $handler(new RunCommandMessage('test:command'));
+
+ $this->assertSame(0, $context->exitCode);
+ $this->assertStringContainsString('some message', $context->output);
+ }
+
+ public function testExecutesCommandThatThrowsException()
+ {
+ $handler = new RunCommandMessageHandler($this->createApplicationWithCommand());
+
+ try {
+ $handler(new RunCommandMessage('test:command --throw'));
+ } catch (RunCommandFailedException $e) {
+ $this->assertSame(1, $e->context->exitCode);
+ $this->assertStringContainsString('some message', $e->context->output);
+ $this->assertInstanceOf(\RuntimeException::class, $e->getPrevious());
+ $this->assertSame('exception message', $e->getMessage());
+
+ return;
+ }
+
+ $this->fail('Exception not thrown.');
+ }
+
+ public function testExecutesCommandThatCatchesThrownException()
+ {
+ $handler = new RunCommandMessageHandler($this->createApplicationWithCommand());
+ $context = $handler(new RunCommandMessage('test:command --throw -v', throwOnFailure: false, catchExceptions: true));
+
+ $this->assertSame(1, $context->exitCode);
+ $this->assertStringContainsString('[RuntimeException]', $context->output);
+ $this->assertStringContainsString('exception message', $context->output);
+ }
+
+ public function testThrowOnNonSuccess()
+ {
+ $handler = new RunCommandMessageHandler($this->createApplicationWithCommand());
+
+ try {
+ $handler(new RunCommandMessage('test:command --exit=1'));
+ } catch (RunCommandFailedException $e) {
+ $this->assertSame(1, $e->context->exitCode);
+ $this->assertStringContainsString('some message', $e->context->output);
+ $this->assertSame('Command "test:command --exit=1" exited with code "1".', $e->getMessage());
+ $this->assertNull($e->getPrevious());
+
+ return;
+ }
+
+ $this->fail('Exception not thrown.');
+ }
+
+ private function createApplicationWithCommand(): Application
+ {
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->addCommands([
+ new class extends Command {
+ public function configure(): void
+ {
+ $this
+ ->setName('test:command')
+ ->addOption('throw')
+ ->addOption('exit', null, InputOption::VALUE_REQUIRED, 0)
+ ;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $output->write('some message');
+
+ if ($input->getOption('throw')) {
+ throw new \RuntimeException('exception message');
+ }
+
+ return (int) $input->getOption('exit');
+ }
+ },
+ ]);
+
+ return $application;
+ }
+}
diff --git a/Tests/Output/AnsiColorModeTest.php b/Tests/Output/AnsiColorModeTest.php
new file mode 100644
index 000000000..eb3e463e8
--- /dev/null
+++ b/Tests/Output/AnsiColorModeTest.php
@@ -0,0 +1,96 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Tests\Output;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Exception\InvalidArgumentException;
+use Symfony\Component\Console\Output\AnsiColorMode;
+
+class AnsiColorModeTest extends TestCase
+{
+ /**
+ * @dataProvider provideColorsConversion
+ */
+ public function testColorsConversionToAnsi4(string $corlorHex, array $expected)
+ {
+ $this->assertSame((string) $expected[AnsiColorMode::Ansi4->name], AnsiColorMode::Ansi4->convertFromHexToAnsiColorCode($corlorHex));
+ }
+
+ /**
+ * @dataProvider provideColorsConversion
+ */
+ public function testColorsConversionToAnsi8(string $corlorHex, array $expected)
+ {
+ $this->assertSame('8;5;'.$expected[AnsiColorMode::Ansi8->name], AnsiColorMode::Ansi8->convertFromHexToAnsiColorCode($corlorHex));
+ }
+
+ public static function provideColorsConversion(): \Generator
+ {
+ yield ['#606702', [
+ AnsiColorMode::Ansi8->name => 100,
+ AnsiColorMode::Ansi4->name => 0,
+ ]];
+
+ yield ['#f40502', [
+ AnsiColorMode::Ansi8->name => 196,
+ AnsiColorMode::Ansi4->name => 1,
+ ]];
+
+ yield ['#2a2a2a', [
+ AnsiColorMode::Ansi8->name => 235,
+ AnsiColorMode::Ansi4->name => 0,
+ ]];
+
+ yield ['#57f70f', [
+ AnsiColorMode::Ansi8->name => 118,
+ AnsiColorMode::Ansi4->name => 2,
+ ]];
+
+ yield ['#eec7fa', [
+ AnsiColorMode::Ansi8->name => 225,
+ AnsiColorMode::Ansi4->name => 7,
+ ]];
+
+ yield ['#a8a8a8', [
+ AnsiColorMode::Ansi8->name => 248,
+ AnsiColorMode::Ansi4->name => 7,
+ ]];
+
+ yield ['#000000', [
+ AnsiColorMode::Ansi8->name => 16,
+ AnsiColorMode::Ansi4->name => 0,
+ ]];
+
+ yield ['#ffffff', [
+ AnsiColorMode::Ansi8->name => 231,
+ AnsiColorMode::Ansi4->name => 7,
+ ]];
+ }
+
+ public function testColorsConversionWithoutSharp()
+ {
+ $this->assertSame('8;5;102', AnsiColorMode::Ansi8->convertFromHexToAnsiColorCode('547869'));
+ }
+
+ public function testColorsConversionWithout3Characters()
+ {
+ $this->assertSame('8;5;241', AnsiColorMode::Ansi8->convertFromHexToAnsiColorCode('#666'));
+ }
+
+ public function testInvalidHexCode()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Invalid "#6666" color.');
+
+ AnsiColorMode::Ansi8->convertFromHexToAnsiColorCode('#6666');
+ }
+}
diff --git a/Tests/Output/ConsoleSectionOutputTest.php b/Tests/Output/ConsoleSectionOutputTest.php
index 206b201bc..6d2fa6c3c 100644
--- a/Tests/Output/ConsoleSectionOutputTest.php
+++ b/Tests/Output/ConsoleSectionOutputTest.php
@@ -22,6 +22,7 @@
class ConsoleSectionOutputTest extends TestCase
{
+ /** @var resource */
private $stream;
protected function setUp(): void
@@ -31,7 +32,7 @@ protected function setUp(): void
protected function tearDown(): void
{
- $this->stream = null;
+ unset($this->stream);
}
public function testClearAll()
@@ -43,7 +44,7 @@ public function testClearAll()
$output->clear();
rewind($output->getStream());
- $this->assertEquals('Foo'.\PHP_EOL.'Bar'.\PHP_EOL.sprintf("\x1b[%dA", 2)."\x1b[0J", stream_get_contents($output->getStream()));
+ $this->assertEquals('Foo'.\PHP_EOL.'Bar'.\PHP_EOL.\sprintf("\x1b[%dA", 2)."\x1b[0J", stream_get_contents($output->getStream()));
}
public function testClearNumberOfLines()
@@ -55,7 +56,7 @@ public function testClearNumberOfLines()
$output->clear(2);
rewind($output->getStream());
- $this->assertEquals("Foo\nBar\nBaz\nFooBar".\PHP_EOL.sprintf("\x1b[%dA", 2)."\x1b[0J", stream_get_contents($output->getStream()));
+ $this->assertEquals("Foo\nBar\nBaz\nFooBar".\PHP_EOL.\sprintf("\x1b[%dA", 2)."\x1b[0J", stream_get_contents($output->getStream()));
}
public function testClearNumberOfLinesWithMultipleSections()
@@ -103,6 +104,90 @@ public function testOverwrite()
$this->assertEquals('Foo'.\PHP_EOL."\x1b[1A\x1b[0JBar".\PHP_EOL, stream_get_contents($output->getStream()));
}
+ public function testMaxHeight()
+ {
+ $expected = '';
+ $sections = [];
+ $output = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter());
+ $output->setMaxHeight(3);
+
+ // fill the section
+ $output->writeln(['One', 'Two', 'Three']);
+ $expected .= 'One'.\PHP_EOL.'Two'.\PHP_EOL.'Three'.\PHP_EOL;
+
+ // cause overflow (redraw whole section, without first line)
+ $output->writeln('Four');
+ $expected .= "\x1b[3A\x1b[0J";
+ $expected .= 'Two'.\PHP_EOL.'Three'.\PHP_EOL.'Four'.\PHP_EOL;
+
+ // cause overflow with multiple new lines at once
+ $output->writeln('Five'.\PHP_EOL.'Six');
+ $expected .= "\x1b[3A\x1b[0J";
+ $expected .= 'Four'.\PHP_EOL.'Five'.\PHP_EOL.'Six'.\PHP_EOL;
+
+ // reset line height (redraw whole section, displaying all lines)
+ $output->setMaxHeight(0);
+ $expected .= "\x1b[3A\x1b[0J";
+ $expected .= 'One'.\PHP_EOL.'Two'.\PHP_EOL.'Three'.\PHP_EOL.'Four'.\PHP_EOL.'Five'.\PHP_EOL.'Six'.\PHP_EOL;
+
+ rewind($output->getStream());
+ $this->assertEquals($expected, stream_get_contents($output->getStream()));
+ }
+
+ public function testMaxHeightMultipleSections()
+ {
+ $expected = '';
+ $sections = [];
+
+ $firstSection = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter());
+ $firstSection->setMaxHeight(3);
+
+ $secondSection = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter());
+ $secondSection->setMaxHeight(3);
+
+ // fill the first section
+ $firstSection->writeln(['One', 'Two', 'Three']);
+ $expected .= 'One'.\PHP_EOL.'Two'.\PHP_EOL.'Three'.\PHP_EOL;
+
+ // fill the second section
+ $secondSection->writeln(['One', 'Two', 'Three']);
+ $expected .= 'One'.\PHP_EOL.'Two'.\PHP_EOL.'Three'.\PHP_EOL;
+
+ // cause overflow of second section (redraw whole section, without first line)
+ $secondSection->writeln('Four');
+ $expected .= "\x1b[3A\x1b[0J";
+ $expected .= 'Two'.\PHP_EOL.'Three'.\PHP_EOL.'Four'.\PHP_EOL;
+
+ // cause overflow of first section (redraw whole section, without first line)
+ $firstSection->writeln('Four'.\PHP_EOL.'Five'.\PHP_EOL.'Six');
+ $expected .= "\x1b[6A\x1b[0J";
+ $expected .= 'Four'.\PHP_EOL.'Five'.\PHP_EOL.'Six'.\PHP_EOL;
+ $expected .= 'Two'.\PHP_EOL.'Three'.\PHP_EOL.'Four'.\PHP_EOL;
+
+ rewind($this->stream);
+ $this->assertEquals(escapeshellcmd($expected), escapeshellcmd(stream_get_contents($this->stream)));
+ }
+
+ public function testMaxHeightWithoutNewLine()
+ {
+ $expected = '';
+ $sections = [];
+ $output = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter());
+ $output->setMaxHeight(3);
+
+ // fill the section
+ $output->writeln(['One', 'Two']);
+ $output->write('Three');
+ $expected .= 'One'.\PHP_EOL.'Two'.\PHP_EOL.'Three'.\PHP_EOL;
+
+ // append text to the last line
+ $output->write(' and Four');
+ $expected .= "\x1b[1A\x1b[0J".'Three and Four'.\PHP_EOL;
+
+ rewind($output->getStream());
+ $this->assertEquals($expected, stream_get_contents($output->getStream()));
+ }
+
public function testOverwriteMultipleLines()
{
$sections = [];
@@ -112,7 +197,7 @@ public function testOverwriteMultipleLines()
$output->overwrite('Bar');
rewind($output->getStream());
- $this->assertEquals('Foo'.\PHP_EOL.'Bar'.\PHP_EOL.'Baz'.\PHP_EOL.sprintf("\x1b[%dA", 3)."\x1b[0J".'Bar'.\PHP_EOL, stream_get_contents($output->getStream()));
+ $this->assertEquals('Foo'.\PHP_EOL.'Bar'.\PHP_EOL.'Baz'.\PHP_EOL.\sprintf("\x1b[%dA", 3)."\x1b[0J".'Bar'.\PHP_EOL, stream_get_contents($output->getStream()));
}
public function testAddingMultipleSections()
@@ -141,6 +226,52 @@ public function testMultipleSectionsOutput()
$this->assertEquals('Foo'.\PHP_EOL.'Bar'.\PHP_EOL."\x1b[2A\x1b[0JBar".\PHP_EOL."\x1b[1A\x1b[0JBaz".\PHP_EOL.'Bar'.\PHP_EOL."\x1b[1A\x1b[0JFoobar".\PHP_EOL, stream_get_contents($output->getStream()));
}
+ public function testMultipleSectionsOutputWithoutNewline()
+ {
+ $expected = '';
+ $output = new StreamOutput($this->stream);
+ $sections = [];
+ $output1 = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter());
+ $output2 = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter());
+
+ $output1->write('Foo');
+ $expected .= 'Foo'.\PHP_EOL;
+ $output2->writeln('Bar');
+ $expected .= 'Bar'.\PHP_EOL;
+
+ $output1->writeln(' is not foo.');
+ $expected .= "\x1b[2A\x1b[0JFoo is not foo.".\PHP_EOL.'Bar'.\PHP_EOL;
+
+ $output2->write('Baz');
+ $expected .= 'Baz'.\PHP_EOL;
+ $output2->write('bar');
+ $expected .= "\x1b[1A\x1b[0JBazbar".\PHP_EOL;
+ $output2->writeln('');
+ $expected .= "\x1b[1A\x1b[0JBazbar".\PHP_EOL;
+ $output2->writeln('');
+ $expected .= \PHP_EOL;
+ $output2->writeln('Done.');
+ $expected .= 'Done.'.\PHP_EOL;
+
+ rewind($output->getStream());
+ $this->assertSame($expected, stream_get_contents($output->getStream()));
+ }
+
+ public function testClearAfterOverwriteClearsCorrectNumberOfLines()
+ {
+ $expected = '';
+ $sections = [];
+ $output = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter());
+
+ $output->overwrite('foo');
+ $expected .= 'foo'.\PHP_EOL;
+ $output->clear();
+ $expected .= "\x1b[1A\x1b[0J";
+
+ rewind($output->getStream());
+ $this->assertSame($expected, stream_get_contents($output->getStream()));
+ }
+
public function testClearSectionContainingQuestion()
{
$inputStream = fopen('php://memory', 'r+', false);
@@ -160,4 +291,16 @@ public function testClearSectionContainingQuestion()
rewind($output->getStream());
$this->assertSame('What\'s your favorite super hero?'.\PHP_EOL."\x1b[2A\x1b[0J", stream_get_contents($output->getStream()));
}
+
+ public function testWriteWithoutNewLine()
+ {
+ $sections = [];
+ $output = new ConsoleSectionOutput($this->stream, $sections, OutputInterface::VERBOSITY_NORMAL, true, new OutputFormatter());
+
+ $output->write('Foo'.\PHP_EOL);
+ $output->write('Bar');
+
+ rewind($output->getStream());
+ $this->assertEquals(escapeshellcmd('Foo'.\PHP_EOL.'Bar'.\PHP_EOL), escapeshellcmd(stream_get_contents($output->getStream())));
+ }
}
diff --git a/Tests/Output/NullOutputTest.php b/Tests/Output/NullOutputTest.php
index 1e0967ea5..4da46cf8f 100644
--- a/Tests/Output/NullOutputTest.php
+++ b/Tests/Output/NullOutputTest.php
@@ -35,10 +35,10 @@ public function testConstructor()
public function testVerbosity()
{
$output = new NullOutput();
- $this->assertSame(OutputInterface::VERBOSITY_QUIET, $output->getVerbosity(), '->getVerbosity() returns VERBOSITY_QUIET for NullOutput by default');
+ $this->assertSame(OutputInterface::VERBOSITY_SILENT, $output->getVerbosity(), '->getVerbosity() returns VERBOSITY_SILENT for NullOutput by default');
$output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
- $this->assertSame(OutputInterface::VERBOSITY_QUIET, $output->getVerbosity(), '->getVerbosity() always returns VERBOSITY_QUIET for NullOutput');
+ $this->assertSame(OutputInterface::VERBOSITY_SILENT, $output->getVerbosity(), '->getVerbosity() always returns VERBOSITY_QUIET for NullOutput');
}
public function testGetFormatter()
@@ -60,7 +60,7 @@ public function testSetVerbosity()
{
$output = new NullOutput();
$output->setVerbosity(Output::VERBOSITY_NORMAL);
- $this->assertEquals(Output::VERBOSITY_QUIET, $output->getVerbosity());
+ $this->assertEquals(Output::VERBOSITY_SILENT, $output->getVerbosity());
}
public function testSetDecorated()
@@ -70,10 +70,16 @@ public function testSetDecorated()
$this->assertFalse($output->isDecorated());
}
+ public function testIsSilent()
+ {
+ $output = new NullOutput();
+ $this->assertTrue($output->isSilent());
+ }
+
public function testIsQuiet()
{
$output = new NullOutput();
- $this->assertTrue($output->isQuiet());
+ $this->assertFalse($output->isQuiet());
}
public function testIsVerbose()
diff --git a/Tests/Output/OutputTest.php b/Tests/Output/OutputTest.php
index f337c4ddd..64e491048 100644
--- a/Tests/Output/OutputTest.php
+++ b/Tests/Output/OutputTest.php
@@ -164,6 +164,7 @@ public function testWriteWithVerbosityOption($verbosity, $expected, $msg)
public static function verbosityProvider()
{
return [
+ [Output::VERBOSITY_SILENT, '', '->write() in SILENT mode never outputs'],
[Output::VERBOSITY_QUIET, '2', '->write() in QUIET mode only outputs when an explicit QUIET verbosity is passed'],
[Output::VERBOSITY_NORMAL, '123', '->write() in NORMAL mode outputs anything below an explicit VERBOSE verbosity'],
[Output::VERBOSITY_VERBOSE, '1234', '->write() in VERBOSE mode outputs anything below an explicit VERY_VERBOSE verbosity'],
@@ -175,14 +176,14 @@ public static function verbosityProvider()
class TestOutput extends Output
{
- public $output = '';
+ public string $output = '';
public function clear()
{
$this->output = '';
}
- protected function doWrite(string $message, bool $newline)
+ protected function doWrite(string $message, bool $newline): void
{
$this->output .= $message.($newline ? "\n" : '');
}
diff --git a/Tests/Output/StreamOutputTest.php b/Tests/Output/StreamOutputTest.php
index 89debf40c..f8c9913bc 100644
--- a/Tests/Output/StreamOutputTest.php
+++ b/Tests/Output/StreamOutputTest.php
@@ -17,6 +17,7 @@
class StreamOutputTest extends TestCase
{
+ /** @var resource */
protected $stream;
protected function setUp(): void
@@ -26,7 +27,7 @@ protected function setUp(): void
protected function tearDown(): void
{
- $this->stream = null;
+ unset($this->stream);
}
public function testConstructor()
diff --git a/Tests/Question/ChoiceQuestionTest.php b/Tests/Question/ChoiceQuestionTest.php
index d8b1f10ec..564dee724 100644
--- a/Tests/Question/ChoiceQuestionTest.php
+++ b/Tests/Question/ChoiceQuestionTest.php
@@ -152,14 +152,14 @@ public function testSelectWithNonStringChoices()
class StringChoice
{
- private $string;
+ private string $string;
public function __construct(string $string)
{
$this->string = $string;
}
- public function __toString()
+ public function __toString(): string
{
return $this->string;
}
diff --git a/Tests/Question/ConfirmationQuestionTest.php b/Tests/Question/ConfirmationQuestionTest.php
index 44f4c870b..bd11047b3 100644
--- a/Tests/Question/ConfirmationQuestionTest.php
+++ b/Tests/Question/ConfirmationQuestionTest.php
@@ -26,7 +26,7 @@ public function testDefaultRegexUsecases($default, $answers, $expected, $message
foreach ($answers as $answer) {
$normalizer = $sut->getNormalizer();
$actual = $normalizer($answer);
- $this->assertEquals($expected, $actual, sprintf($message, $answer));
+ $this->assertEquals($expected, $actual, \sprintf($message, $answer));
}
}
diff --git a/Tests/Question/QuestionTest.php b/Tests/Question/QuestionTest.php
index bf2763d77..15d8212b9 100644
--- a/Tests/Question/QuestionTest.php
+++ b/Tests/Question/QuestionTest.php
@@ -16,11 +16,10 @@
class QuestionTest extends TestCase
{
- private $question;
+ private Question $question;
protected function setUp(): void
{
- parent::setUp();
$this->question = new Question('Test question');
}
@@ -62,7 +61,7 @@ public function testIsHiddenDefault()
public function testSetHiddenWithAutocompleterCallback()
{
$this->question->setAutocompleterCallback(
- function (string $input): array { return []; }
+ fn (string $input): array => []
);
$this->expectException(\LogicException::class);
@@ -76,7 +75,7 @@ function (string $input): array { return []; }
public function testSetHiddenWithNoAutocompleterCallback()
{
$this->question->setAutocompleterCallback(
- function (string $input): array { return []; }
+ fn (string $input): array => []
);
$this->question->setAutocompleterCallback(null);
@@ -187,7 +186,7 @@ public function testGetAutocompleterValuesDefault()
public function testGetSetAutocompleterCallback()
{
- $callback = function (string $input): array { return []; };
+ $callback = fn (string $input): array => [];
$this->question->setAutocompleterCallback($callback);
self::assertSame($callback, $this->question->getAutocompleterCallback());
@@ -208,7 +207,7 @@ public function testSetAutocompleterCallbackWhenHidden()
);
$this->question->setAutocompleterCallback(
- function (string $input): array { return []; }
+ fn (string $input): array => []
);
}
@@ -220,7 +219,7 @@ public function testSetAutocompleterCallbackWhenNotHidden()
$exception = null;
try {
$this->question->setAutocompleterCallback(
- function (string $input): array { return []; }
+ fn (string $input): array => []
);
} catch (\Exception $exception) {
// Do nothing
@@ -232,7 +231,7 @@ function (string $input): array { return []; }
public static function providerGetSetValidator()
{
return [
- [function ($input) { return $input; }],
+ [fn ($input) => $input],
[null],
];
}
@@ -288,7 +287,7 @@ public function testGetMaxAttemptsDefault()
public function testGetSetNormalizer()
{
- $normalizer = function ($input) { return $input; };
+ $normalizer = fn ($input) => $input;
$this->question->setNormalizer($normalizer);
self::assertSame($normalizer, $this->question->getNormalizer());
}
diff --git a/Tests/SignalRegistry/SignalMapTest.php b/Tests/SignalRegistry/SignalMapTest.php
new file mode 100644
index 000000000..f4e320477
--- /dev/null
+++ b/Tests/SignalRegistry/SignalMapTest.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Tests\SignalRegistry;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\SignalRegistry\SignalMap;
+
+class SignalMapTest extends TestCase
+{
+ /**
+ * @requires extension pcntl
+ *
+ * @testWith [2, "SIGINT"]
+ * [9, "SIGKILL"]
+ * [15, "SIGTERM"]
+ * [31, "SIGSYS"]
+ */
+ public function testSignalExists(int $signal, string $expected)
+ {
+ $this->assertSame($expected, SignalMap::getSignalName($signal));
+ }
+
+ public function testSignalDoesNotExist()
+ {
+ $this->assertNull(SignalMap::getSignalName(999999));
+ }
+}
diff --git a/Tests/SignalRegistry/SignalRegistryTest.php b/Tests/SignalRegistry/SignalRegistryTest.php
index f1ac7c690..92d500f9e 100644
--- a/Tests/SignalRegistry/SignalRegistryTest.php
+++ b/Tests/SignalRegistry/SignalRegistryTest.php
@@ -22,8 +22,12 @@ class SignalRegistryTest extends TestCase
protected function tearDown(): void
{
pcntl_async_signals(false);
+ // We reset all signals to their default value to avoid side effects
+ pcntl_signal(\SIGINT, \SIG_DFL);
+ pcntl_signal(\SIGTERM, \SIG_DFL);
pcntl_signal(\SIGUSR1, \SIG_DFL);
pcntl_signal(\SIGUSR2, \SIG_DFL);
+ pcntl_signal(\SIGALRM, \SIG_DFL);
}
public function testOneCallbackForASignalSignalIsHandled()
diff --git a/Tests/Style/SymfonyStyleTest.php b/Tests/Style/SymfonyStyleTest.php
index 621767bf7..0b40c7c3f 100644
--- a/Tests/Style/SymfonyStyleTest.php
+++ b/Tests/Style/SymfonyStyleTest.php
@@ -16,21 +16,21 @@
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Input\Input;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Output\StreamOutput;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Tester\CommandTester;
class SymfonyStyleTest extends TestCase
{
- /** @var Command */
- protected $command;
- /** @var CommandTester */
- protected $tester;
- private $colSize;
+ protected Command $command;
+ protected CommandTester $tester;
+ private string|false $colSize;
protected function setUp(): void
{
@@ -43,8 +43,6 @@ protected function setUp(): void
protected function tearDown(): void
{
putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS');
- $this->command = null;
- $this->tester = null;
}
/**
@@ -181,4 +179,44 @@ public function testMemoryConsumption()
$this->assertSame(0, memory_get_usage() - $start);
}
+
+ public function testAskAndClearExpectFullSectionCleared()
+ {
+ $answer = 'Answer';
+ $inputStream = fopen('php://memory', 'r+');
+ fwrite($inputStream, $answer.\PHP_EOL);
+ rewind($inputStream);
+ $input = $this->createMock(Input::class);
+ $sections = [];
+ $output = new ConsoleSectionOutput(fopen('php://memory', 'r+', false), $sections, StreamOutput::VERBOSITY_NORMAL, true, new OutputFormatter());
+ $input
+ ->method('isInteractive')
+ ->willReturn(true);
+ $input
+ ->method('getStream')
+ ->willReturn($inputStream);
+
+ $style = new SymfonyStyle($input, $output);
+
+ $style->writeln('start');
+ $style->write('foo');
+ $style->writeln(' and bar');
+ $givenAnswer = $style->ask('Dummy question?');
+ $style->write('foo2'.\PHP_EOL);
+ $output->write('bar2');
+ $output->clear();
+
+ rewind($output->getStream());
+ $this->assertEquals($answer, $givenAnswer);
+ $this->assertEquals(escapeshellcmd(
+ 'start'.\PHP_EOL. // write start
+ 'foo'.\PHP_EOL. // write foo
+ "\x1b[1A\x1b[0Jfoo and bar".\PHP_EOL. // complete line
+ \PHP_EOL." \033[32mDummy question?\033[39m:".\PHP_EOL.' > '.\PHP_EOL.\PHP_EOL. // question
+ 'foo2'.\PHP_EOL. // write foo2
+ 'bar2'.\PHP_EOL. // write bar
+ "\033[9A\033[0J"), // clear 9 lines (8 output lines and one from the answer input return)
+ escapeshellcmd(stream_get_contents($output->getStream()))
+ );
+ }
}
diff --git a/Tests/TerminalTest.php b/Tests/TerminalTest.php
index 08b0df7cd..d43469d12 100644
--- a/Tests/TerminalTest.php
+++ b/Tests/TerminalTest.php
@@ -12,13 +12,14 @@
namespace Symfony\Component\Console\Tests;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Output\AnsiColorMode;
use Symfony\Component\Console\Terminal;
class TerminalTest extends TestCase
{
- private $colSize;
- private $lineSize;
- private $ansiCon;
+ private string|false $colSize;
+ private string|false $lineSize;
+ private string|false $ansiCon;
protected function setUp(): void
{
@@ -40,7 +41,6 @@ private function resetStatics()
{
foreach (['height', 'width', 'stty'] as $name) {
$property = new \ReflectionProperty(Terminal::class, $name);
- $property->setAccessible(true);
$property->setValue(null, null);
}
}
@@ -94,4 +94,58 @@ public function testSttyOnWindows()
$terminal = new Terminal();
$this->assertSame((int) $matches[1], $terminal->getWidth());
}
+
+ /**
+ * @dataProvider provideTerminalColorEnv
+ */
+ public function testGetColorMode(?string $testColorTerm, ?string $testTerm, AnsiColorMode $expected)
+ {
+ $oriColorTerm = getenv('COLORTERM');
+ $oriTerm = getenv('TERM');
+
+ try {
+ putenv($testColorTerm ? "COLORTERM={$testColorTerm}" : 'COLORTERM');
+ putenv($testTerm ? "TERM={$testTerm}" : 'TERM');
+
+ $this->assertSame($expected, Terminal::getColorMode());
+ } finally {
+ (false !== $oriColorTerm) ? putenv('COLORTERM='.$oriColorTerm) : putenv('COLORTERM');
+ (false !== $oriTerm) ? putenv('TERM='.$oriTerm) : putenv('TERM');
+ Terminal::setColorMode(null);
+ }
+ }
+
+ public static function provideTerminalColorEnv(): \Generator
+ {
+ yield ['truecolor', null, AnsiColorMode::Ansi24];
+ yield ['TRUECOLOR', null, AnsiColorMode::Ansi24];
+ yield ['somethingLike256Color', null, AnsiColorMode::Ansi8];
+ yield [null, 'xterm-truecolor', AnsiColorMode::Ansi24];
+ yield [null, 'xterm-TRUECOLOR', AnsiColorMode::Ansi24];
+ yield [null, 'xterm-256color', AnsiColorMode::Ansi8];
+ yield [null, 'xterm-256COLOR', AnsiColorMode::Ansi8];
+ yield [null, null, Terminal::DEFAULT_COLOR_MODE];
+ }
+
+ public function testSetColorMode()
+ {
+ $oriColorTerm = getenv('COLORTERM');
+ $oriTerm = getenv('TERM');
+
+ try {
+ putenv('COLORTERM');
+ putenv('TERM');
+ $this->assertSame(Terminal::DEFAULT_COLOR_MODE, Terminal::getColorMode());
+
+ putenv('COLORTERM=256color');
+ $this->assertSame(Terminal::DEFAULT_COLOR_MODE, Terminal::getColorMode()); // Terminal color mode is cached at first call. Terminal cannot change during execution.
+
+ Terminal::setColorMode(AnsiColorMode::Ansi24); // Force change by user.
+ $this->assertSame(AnsiColorMode::Ansi24, Terminal::getColorMode());
+ } finally {
+ (false !== $oriColorTerm) ? putenv('COLORTERM='.$oriColorTerm) : putenv('COLORTERM');
+ (false !== $oriTerm) ? putenv('TERM='.$oriTerm) : putenv('TERM');
+ Terminal::setColorMode(null);
+ }
+ }
}
diff --git a/Tests/Tester/ApplicationTesterTest.php b/Tests/Tester/ApplicationTesterTest.php
index 024e32a00..f43775179 100644
--- a/Tests/Tester/ApplicationTesterTest.php
+++ b/Tests/Tester/ApplicationTesterTest.php
@@ -20,8 +20,8 @@
class ApplicationTesterTest extends TestCase
{
- protected $application;
- protected $tester;
+ protected Application $application;
+ protected ApplicationTester $tester;
protected function setUp(): void
{
@@ -38,12 +38,6 @@ protected function setUp(): void
$this->tester->run(['command' => 'foo', 'foo' => 'bar'], ['interactive' => false, 'decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE]);
}
- protected function tearDown(): void
- {
- $this->application = null;
- $this->tester = null;
- }
-
public function testRun()
{
$this->assertFalse($this->tester->getInput()->isInteractive(), '->execute() takes an interactive option');
diff --git a/Tests/Tester/CommandTesterTest.php b/Tests/Tester/CommandTesterTest.php
index d3c644340..ce0a24b99 100644
--- a/Tests/Tester/CommandTesterTest.php
+++ b/Tests/Tester/CommandTesterTest.php
@@ -24,8 +24,8 @@
class CommandTesterTest extends TestCase
{
- protected $command;
- protected $tester;
+ protected Command $command;
+ protected CommandTester $tester;
protected function setUp(): void
{
@@ -38,12 +38,6 @@ protected function setUp(): void
$this->tester->execute(['foo' => 'bar'], ['interactive' => false, 'decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE]);
}
- protected function tearDown(): void
- {
- $this->command = null;
- $this->tester = null;
- }
-
public function testExecute()
{
$this->assertFalse($this->tester->getInput()->isInteractive(), '->execute() takes an interactive option');
@@ -160,8 +154,6 @@ public function testCommandWithDefaultInputs()
public function testCommandWithWrongInputsNumber()
{
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('Aborted.');
$questions = [
'What\'s your name?',
'How are you?',
@@ -180,13 +172,15 @@ public function testCommandWithWrongInputsNumber()
$tester = new CommandTester($command);
$tester->setInputs(['a', 'Bobby', 'Fine']);
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('Aborted.');
+
$tester->execute([]);
}
public function testCommandWithQuestionsButNoInputs()
{
- $this->expectException(\RuntimeException::class);
- $this->expectExceptionMessage('Aborted.');
$questions = [
'What\'s your name?',
'How are you?',
@@ -204,6 +198,10 @@ public function testCommandWithQuestionsButNoInputs()
});
$tester = new CommandTester($command);
+
+ $this->expectException(\RuntimeException::class);
+ $this->expectExceptionMessage('Aborted.');
+
$tester->execute([]);
}
diff --git a/Tests/Tester/Constraint/CommandIsSuccessfulTest.php b/Tests/Tester/Constraint/CommandIsSuccessfulTest.php
index 7a2b4c719..61ab5d0f8 100644
--- a/Tests/Tester/Constraint/CommandIsSuccessfulTest.php
+++ b/Tests/Tester/Constraint/CommandIsSuccessfulTest.php
@@ -13,7 +13,6 @@
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
-use PHPUnit\Framework\TestFailure;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Tester\Constraint\CommandIsSuccessful;
@@ -35,16 +34,9 @@ public function testUnsuccessfulCommand(string $expectedException, int $exitCode
{
$constraint = new CommandIsSuccessful();
- try {
- $constraint->evaluate($exitCode);
- } catch (ExpectationFailedException $e) {
- $this->assertStringContainsString('Failed asserting that the command is successful.', TestFailure::exceptionToString($e));
- $this->assertStringContainsString($expectedException, TestFailure::exceptionToString($e));
-
- return;
- }
-
- $this->fail();
+ $this->expectException(ExpectationFailedException::class);
+ $this->expectExceptionMessageMatches('/Failed asserting that the command is successful\..*'.$expectedException.'/s');
+ $constraint->evaluate($exitCode);
}
public static function providesUnsuccessful(): iterable
diff --git a/Tests/phpt/alarm/command_exit.phpt b/Tests/phpt/alarm/command_exit.phpt
new file mode 100644
index 000000000..d3015ad9d
--- /dev/null
+++ b/Tests/phpt/alarm/command_exit.phpt
@@ -0,0 +1,64 @@
+--TEST--
+Test command that exits
+--SKIPIF--
+
+--FILE--
+getApplication()->setAlarmInterval(1);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ sleep(5);
+
+ $output->writeln('should not be displayed');
+
+ return 0;
+ }
+
+ public function getSubscribedSignals(): array
+ {
+ return [\SIGALRM];
+ }
+
+ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
+ {
+ if (\SIGALRM === $signal) {
+ echo "Received alarm!";
+
+ return 0;
+ }
+
+ return false;
+ }
+}
+
+$app = new Application();
+$app->setDispatcher(new \Symfony\Component\EventDispatcher\EventDispatcher());
+$app->add(new MyCommand('foo'));
+
+$app
+ ->setDefaultCommand('foo', true)
+ ->run()
+;
+--EXPECT--
+Received alarm!
diff --git a/Tests/phpt/signal/command_exit.phpt b/Tests/phpt/signal/command_exit.phpt
new file mode 100644
index 000000000..379476189
--- /dev/null
+++ b/Tests/phpt/signal/command_exit.phpt
@@ -0,0 +1,56 @@
+--TEST--
+Test command that exits
+--SKIPIF--
+
+--FILE--
+writeln('should not be displayed');
+
+ return 0;
+ }
+
+
+ public function getSubscribedSignals(): array
+ {
+ return [\SIGINT];
+ }
+
+ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
+ {
+ echo "Received signal!";
+
+ return 0;
+ }
+}
+
+$app = new Application();
+$app->setDispatcher(new \Symfony\Component\EventDispatcher\EventDispatcher());
+$app->add(new MyCommand('foo'));
+
+$app
+ ->setDefaultCommand('foo', true)
+ ->run()
+;
+--EXPECT--
+Received signal!
diff --git a/Tests/phpt/single_application/help_name.phpt b/Tests/phpt/single_application/help_name.phpt
index f3d220b72..3291c83c5 100644
--- a/Tests/phpt/single_application/help_name.phpt
+++ b/Tests/phpt/single_application/help_name.phpt
@@ -29,7 +29,8 @@ Usage:
Options:
-h, --help Display help for the given command. When no command is given display help for the %s command
- -q, --quiet Do not output any message
+ --silent Do not output any message
+ -q, --quiet Only errors are displayed. All other output is suppressed
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
diff --git a/composer.json b/composer.json
index 4fa4964a1..0ed1bd9af 100644
--- a/composer.json
+++ b/composer.json
@@ -16,39 +16,33 @@
}
],
"require": {
- "php": ">=7.2.5",
- "symfony/deprecation-contracts": "^2.1|^3",
+ "php": ">=8.2",
"symfony/polyfill-mbstring": "~1.0",
- "symfony/polyfill-php73": "^1.9",
- "symfony/polyfill-php80": "^1.16",
- "symfony/service-contracts": "^1.1|^2|^3",
- "symfony/string": "^5.1|^6.0"
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/string": "^6.4|^7.0"
},
"require-dev": {
- "symfony/config": "^4.4|^5.0|^6.0",
- "symfony/event-dispatcher": "^4.4|^5.0|^6.0",
- "symfony/dependency-injection": "^4.4|^5.0|^6.0",
- "symfony/lock": "^4.4|^5.0|^6.0",
- "symfony/process": "^4.4|^5.0|^6.0",
- "symfony/var-dumper": "^4.4|^5.0|^6.0",
- "psr/log": "^1|^2"
+ "symfony/config": "^6.4|^7.0",
+ "symfony/event-dispatcher": "^6.4|^7.0",
+ "symfony/http-foundation": "^6.4|^7.0",
+ "symfony/http-kernel": "^6.4|^7.0",
+ "symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/lock": "^6.4|^7.0",
+ "symfony/messenger": "^6.4|^7.0",
+ "symfony/process": "^6.4|^7.0",
+ "symfony/stopwatch": "^6.4|^7.0",
+ "symfony/var-dumper": "^6.4|^7.0",
+ "psr/log": "^1|^2|^3"
},
"provide": {
- "psr/log-implementation": "1.0|2.0"
- },
- "suggest": {
- "symfony/event-dispatcher": "",
- "symfony/lock": "",
- "symfony/process": "",
- "psr/log": "For using the console logger"
+ "psr/log-implementation": "1.0|2.0|3.0"
},
"conflict": {
- "psr/log": ">=3",
- "symfony/dependency-injection": "<4.4",
- "symfony/dotenv": "<5.1",
- "symfony/event-dispatcher": "<4.4",
- "symfony/lock": "<4.4",
- "symfony/process": "<4.4"
+ "symfony/dependency-injection": "<6.4",
+ "symfony/dotenv": "<6.4",
+ "symfony/event-dispatcher": "<6.4",
+ "symfony/lock": "<6.4",
+ "symfony/process": "<6.4"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Console\\": "" },
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 15e7e52a9..0e96921be 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,7 +1,7 @@
-
-
+
+
./
-
- ./Resources
- ./Tests
- ./vendor
-
-
-
+
+
+ ./Resources
+ ./Tests
+ ./vendor
+
+