diff --git a/.gitattributes b/.gitattributes
index 84c7add05..14c3c3594 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,4 +1,3 @@
/Tests export-ignore
/phpunit.xml.dist export-ignore
-/.gitattributes export-ignore
-/.gitignore export-ignore
+/.git* export-ignore
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..4689c4dad
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,8 @@
+Please do not submit any Pull Requests here. They will be closed.
+---
+
+Please submit your PR here instead:
+https://github.com/symfony/symfony
+
+This repository is what we call a "subtree split": a read-only subset of that main repository.
+We're looking forward to your PR there!
diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml
new file mode 100644
index 000000000..e55b47817
--- /dev/null
+++ b/.github/workflows/close-pull-request.yml
@@ -0,0 +1,20 @@
+name: Close Pull Request
+
+on:
+ pull_request_target:
+ types: [opened]
+
+jobs:
+ run:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: superbrothers/close-pull-request@v3
+ with:
+ comment: |
+ Thanks for your Pull Request! We love contributions.
+
+ However, you should instead open your PR on the main repository:
+ https://github.com/symfony/symfony
+
+ This repository is what we call a "subtree split": a read-only subset of that main repository.
+ We're looking forward to your PR there!
diff --git a/Application.php b/Application.php
index 07cc6d674..1ea644df0 100644
--- a/Application.php
+++ b/Application.php
@@ -17,11 +17,11 @@
use Symfony\Component\Console\Command\HelpCommand;
use Symfony\Component\Console\Command\LazyCommand;
use Symfony\Component\Console\Command\ListCommand;
-use Symfony\Component\Console\Command\SignalableCommandInterface;
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;
@@ -75,8 +75,6 @@ class Application implements ResetInterface
private array $commands = [];
private bool $wantHelps = false;
private ?Command $runningCommand = null;
- private string $name;
- private string $version;
private ?CommandLoaderInterface $commandLoader = null;
private bool $catchExceptions = true;
private bool $catchErrors = false;
@@ -90,16 +88,17 @@ class Application implements ResetInterface
private bool $initialized = false;
private ?SignalRegistry $signalRegistry = null;
private array $signalsToDispatchEvent = [];
+ private ?int $alarmInterval = null;
- public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
- {
- $this->name = $name;
- $this->version = $version;
+ 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];
}
}
@@ -130,6 +129,30 @@ 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.
*
@@ -137,7 +160,7 @@ public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent): void
*
* @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}.
*/
- public function run(InputInterface $input = null, OutputInterface $output = null): int
+ public function run(?InputInterface $input = null, ?OutputInterface $output = null): int
{
if (\function_exists('putenv')) {
@putenv('LINES='.$this->terminal->getHeight());
@@ -163,9 +186,11 @@ public function run(InputInterface $input = null, OutputInterface $output = null
}
}
- $this->configureIO($input, $output);
+ $prevShellVerbosity = getenv('SHELL_VERBOSITY');
try {
+ $this->configureIO($input, $output);
+
$exitCode = $this->doRun($input, $output);
} catch (\Throwable $e) {
if ($e instanceof \Exception && !$this->catchExceptions) {
@@ -200,6 +225,22 @@ public function run(InputInterface $input = null, OutputInterface $output = null
$phpHandler[0]->setExceptionHandler($finalHandler);
}
}
+
+ // SHELL_VERBOSITY is set by Application::configureIO so we need to unset/reset it
+ // to its previous value to avoid one command verbosity to spread to other commands
+ if (false === $prevShellVerbosity) {
+ if (\function_exists('putenv')) {
+ @putenv('SHELL_VERBOSITY');
+ }
+ unset($_ENV['SHELL_VERBOSITY']);
+ unset($_SERVER['SHELL_VERBOSITY']);
+ } else {
+ if (\function_exists('putenv')) {
+ @putenv('SHELL_VERBOSITY='.$prevShellVerbosity);
+ }
+ $_ENV['SHELL_VERBOSITY'] = $prevShellVerbosity;
+ $_SERVER['SHELL_VERBOSITY'] = $prevShellVerbosity;
+ }
}
if ($this->autoExit) {
@@ -264,9 +305,9 @@ public function doRun(InputInterface $input, OutputInterface $output): int
$style = new SymfonyStyle($input, $output);
$output->writeln('');
- $formattedBlock = (new FormatterHelper())->formatBlock(sprintf('Command "%s" is not defined.', $name), 'error', true);
+ $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 (!$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);
@@ -385,8 +426,6 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti
if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) {
$suggestions->suggestOptions($this->getDefinition()->getOptions());
-
- return;
}
}
@@ -477,7 +516,7 @@ 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();
@@ -532,7 +571,7 @@ public function add(Command $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;
@@ -554,12 +593,12 @@ 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];
@@ -622,8 +661,8 @@ public function findNamespace(string $namespace): string
$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)) {
@@ -640,7 +679,7 @@ public function findNamespace(string $namespace): string
$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);
@@ -676,18 +715,18 @@ public function find(string $name): Command
$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
@@ -716,7 +755,7 @@ public function find(string $name): Command
$aliases[$nameOrAlias] = $commandName;
- return $commandName === $nameOrAlias || !\in_array($commandName, $commands);
+ return $commandName === $nameOrAlias || !\in_array($commandName, $commands, true);
}));
}
@@ -742,14 +781,14 @@ public function find(string $name): Command
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;
@@ -762,7 +801,7 @@ public function find(string $name): Command
*
* @return Command[]
*/
- public function all(string $namespace = null): array
+ public function all(?string $namespace = null): array
{
$this->init();
@@ -824,7 +863,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);
}
}
@@ -835,14 +874,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]++/', fn ($m) => 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;
@@ -859,14 +898,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[] = '';
@@ -893,7 +932,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);
@@ -917,6 +956,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;
@@ -934,7 +976,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 {
@@ -950,7 +995,7 @@ protected function configureIO(InputInterface $input, OutputInterface $output):
}
}
- if (-1 === $shellVerbosity) {
+ if (0 > $shellVerbosity) {
$input->setInteractive(false);
}
@@ -977,36 +1022,48 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
}
}
- $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
- if ($commandSignals || $this->dispatcher && $this->signalsToDispatchEvent) {
- 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.');
- }
+ if (($commandSignals = $command->getSubscribedSignals()) || $this->dispatcher && $this->signalsToDispatchEvent) {
+ $signalRegistry = $this->getSignalRegistry();
if (Terminal::hasSttyAvailable()) {
$sttyMode = shell_exec('stty -g');
- foreach ([\SIGINT, \SIGTERM] as $signal) {
- $this->signalRegistry->register($signal, static fn () => shell_exec('stty '.$sttyMode));
+ foreach ([\SIGINT, \SIGQUIT, \SIGTERM] as $signal) {
+ $signalRegistry->register($signal, static fn () => shell_exec('stty '.$sttyMode));
}
}
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);
-
- $this->signalRegistry->register($signal, function ($signal) use ($event, $command, $commandSignals) {
- $this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
- $exitCode = $event->getExitCode();
+ $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();
+ }
// If the command is signalable, we call the handleSignal() method
if (\in_array($signal, $commandSignals, true)) {
$exitCode = $command->handleSignal($signal, $exitCode);
}
+ if (\SIGALRM === $signal) {
+ $this->scheduleAlarm();
+ }
+
if (false !== $exitCode) {
- $event = new ConsoleTerminateEvent($command, $event->getInput(), $event->getOutput(), $exitCode, $signal);
+ $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode, $signal);
$this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE);
exit($event->getExitCode());
@@ -1019,7 +1076,11 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
}
foreach ($commandSignals as $signal) {
- $this->signalRegistry->register($signal, function (int $signal) use ($command): void {
+ $signalRegistry->register($signal, function (int $signal) use ($command): void {
+ if (\SIGALRM === $signal) {
+ $this->scheduleAlarm();
+ }
+
if (false !== $exitCode = $command->handleSignal($signal)) {
exit($exitCode);
}
@@ -1086,7 +1147,8 @@ 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),
@@ -1130,7 +1192,7 @@ private function getAbbreviationSuggestions(array $abbrevs): string
*
* This method is not part of public API and should not be used directly.
*/
- public function extractNamespace(string $name, int $limit = null): string
+ public function extractNamespace(string $name, ?int $limit = null): string
{
$parts = explode(':', $name, -1);
@@ -1231,7 +1293,7 @@ private function splitStringByWidth(string $string, int $width): array
foreach (preg_split('//u', $m[0]) as $char) {
// test if $char could be appended to current line
- if (mb_strwidth($line.$char, 'utf8') <= $width) {
+ if (Helper::width($line.$char) <= $width) {
$line .= $char;
continue;
}
diff --git a/Attribute/Argument.php b/Attribute/Argument.php
new file mode 100644
index 000000000..e6a94d2f1
--- /dev/null
+++ b/Attribute/Argument.php
@@ -0,0 +1,110 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Attribute;
+
+use Symfony\Component\Console\Completion\CompletionInput;
+use Symfony\Component\Console\Completion\Suggestion;
+use Symfony\Component\Console\Exception\LogicException;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\String\UnicodeString;
+
+#[\Attribute(\Attribute::TARGET_PARAMETER)]
+class Argument
+{
+ private const ALLOWED_TYPES = ['string', 'bool', 'int', 'float', 'array'];
+
+ private string|bool|int|float|array|null $default = null;
+ private array|\Closure $suggestedValues;
+ private ?int $mode = null;
+ private string $function = '';
+
+ /**
+ * Represents a console command definition.
+ *
+ * If unset, the `name` value will be inferred from the parameter definition.
+ *
+ * @param array|callable(CompletionInput):list $suggestedValues The values used for input completion
+ */
+ public function __construct(
+ public string $description = '',
+ public string $name = '',
+ array|callable $suggestedValues = [],
+ ) {
+ $this->suggestedValues = \is_callable($suggestedValues) ? $suggestedValues(...) : $suggestedValues;
+ }
+
+ /**
+ * @internal
+ */
+ public static function tryFrom(\ReflectionParameter $parameter): ?self
+ {
+ /** @var self $self */
+ if (null === $self = ($parameter->getAttributes(self::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)?->newInstance()) {
+ return null;
+ }
+
+ if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) {
+ $self->function = $function->class.'::'.$function->name;
+ } else {
+ $self->function = $function->name;
+ }
+
+ $type = $parameter->getType();
+ $name = $parameter->getName();
+
+ if (!$type instanceof \ReflectionNamedType) {
+ throw new LogicException(\sprintf('The parameter "$%s" of "%s()" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $name, $self->function));
+ }
+
+ $parameterTypeName = $type->getName();
+
+ if (!\in_array($parameterTypeName, self::ALLOWED_TYPES, true)) {
+ throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command argument. Only "%s" types are allowed.', $parameterTypeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES)));
+ }
+
+ if (!$self->name) {
+ $self->name = (new UnicodeString($name))->kebab();
+ }
+
+ $self->default = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
+
+ $self->mode = $parameter->isDefaultValueAvailable() || $parameter->allowsNull() ? InputArgument::OPTIONAL : InputArgument::REQUIRED;
+ if ('array' === $parameterTypeName) {
+ $self->mode |= InputArgument::IS_ARRAY;
+ }
+
+ if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $parameter->getDeclaringFunction()->getClosureThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) {
+ $self->suggestedValues = [$instance, $self->suggestedValues[1]];
+ }
+
+ return $self;
+ }
+
+ /**
+ * @internal
+ */
+ public function toInputArgument(): InputArgument
+ {
+ $suggestedValues = \is_callable($this->suggestedValues) ? ($this->suggestedValues)(...) : $this->suggestedValues;
+
+ return new InputArgument($this->name, $this->mode, $this->description, $this->default, $suggestedValues);
+ }
+
+ /**
+ * @internal
+ */
+ public function resolveValue(InputInterface $input): mixed
+ {
+ return $input->getArgument($this->name);
+ }
+}
diff --git a/Attribute/AsCommand.php b/Attribute/AsCommand.php
index b337f548f..767d46ebb 100644
--- a/Attribute/AsCommand.php
+++ b/Attribute/AsCommand.php
@@ -13,15 +13,25 @@
/**
* Service tag to autoconfigure commands.
+ *
+ * @final since Symfony 7.3
*/
#[\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
+ * @param string|null $help The help content of the command, displayed with the help page
+ */
public function __construct(
public string $name,
public ?string $description = null,
array $aliases = [],
bool $hidden = false,
+ public ?string $help = null,
) {
if (!$hidden && !$aliases) {
return;
diff --git a/Attribute/Option.php b/Attribute/Option.php
new file mode 100644
index 000000000..788353463
--- /dev/null
+++ b/Attribute/Option.php
@@ -0,0 +1,181 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Attribute;
+
+use Symfony\Component\Console\Completion\CompletionInput;
+use Symfony\Component\Console\Completion\Suggestion;
+use Symfony\Component\Console\Exception\LogicException;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\String\UnicodeString;
+
+#[\Attribute(\Attribute::TARGET_PARAMETER)]
+class Option
+{
+ private const ALLOWED_TYPES = ['string', 'bool', 'int', 'float', 'array'];
+ private const ALLOWED_UNION_TYPES = ['bool|string', 'bool|int', 'bool|float'];
+
+ private string|bool|int|float|array|null $default = null;
+ private array|\Closure $suggestedValues;
+ private ?int $mode = null;
+ private string $typeName = '';
+ private bool $allowNull = false;
+ private string $function = '';
+
+ /**
+ * Represents a console command --option definition.
+ *
+ * If unset, the `name` value will be inferred from the parameter definition.
+ *
+ * @param array|string|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
+ * @param array|callable(CompletionInput):list $suggestedValues The values used for input completion
+ */
+ public function __construct(
+ public string $description = '',
+ public string $name = '',
+ public array|string|null $shortcut = null,
+ array|callable $suggestedValues = [],
+ ) {
+ $this->suggestedValues = \is_callable($suggestedValues) ? $suggestedValues(...) : $suggestedValues;
+ }
+
+ /**
+ * @internal
+ */
+ public static function tryFrom(\ReflectionParameter $parameter): ?self
+ {
+ /** @var self $self */
+ if (null === $self = ($parameter->getAttributes(self::class, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)?->newInstance()) {
+ return null;
+ }
+
+ if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) {
+ $self->function = $function->class.'::'.$function->name;
+ } else {
+ $self->function = $function->name;
+ }
+
+ $name = $parameter->getName();
+ $type = $parameter->getType();
+
+ if (!$parameter->isDefaultValueAvailable()) {
+ throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must declare a default value.', $name, $self->function));
+ }
+
+ if (!$self->name) {
+ $self->name = (new UnicodeString($name))->kebab();
+ }
+
+ $self->default = $parameter->getDefaultValue();
+ $self->allowNull = $parameter->allowsNull();
+
+ if ($type instanceof \ReflectionUnionType) {
+ return $self->handleUnion($type);
+ }
+
+ if (!$type instanceof \ReflectionNamedType) {
+ throw new LogicException(\sprintf('The parameter "$%s" of "%s()" must have a named type. Untyped or Intersection types are not supported for command options.', $name, $self->function));
+ }
+
+ $self->typeName = $type->getName();
+
+ if (!\in_array($self->typeName, self::ALLOWED_TYPES, true)) {
+ throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command option. Only "%s" types are allowed.', $self->typeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES)));
+ }
+
+ if ('bool' === $self->typeName && $self->allowNull && \in_array($self->default, [true, false], true)) {
+ throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must not be nullable when it has a default boolean value.', $name, $self->function));
+ }
+
+ if ($self->allowNull && null !== $self->default) {
+ throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must either be not-nullable or have a default of null.', $name, $self->function));
+ }
+
+ if ('bool' === $self->typeName) {
+ $self->mode = InputOption::VALUE_NONE;
+ if (false !== $self->default) {
+ $self->mode |= InputOption::VALUE_NEGATABLE;
+ }
+ } elseif ('array' === $self->typeName) {
+ $self->mode = InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY;
+ } else {
+ $self->mode = InputOption::VALUE_REQUIRED;
+ }
+
+ if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $parameter->getDeclaringFunction()->getClosureThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) {
+ $self->suggestedValues = [$instance, $self->suggestedValues[1]];
+ }
+
+ return $self;
+ }
+
+ /**
+ * @internal
+ */
+ public function toInputOption(): InputOption
+ {
+ $default = InputOption::VALUE_NONE === (InputOption::VALUE_NONE & $this->mode) ? null : $this->default;
+ $suggestedValues = \is_callable($this->suggestedValues) ? ($this->suggestedValues)(...) : $this->suggestedValues;
+
+ return new InputOption($this->name, $this->shortcut, $this->mode, $this->description, $default, $suggestedValues);
+ }
+
+ /**
+ * @internal
+ */
+ public function resolveValue(InputInterface $input): mixed
+ {
+ $value = $input->getOption($this->name);
+
+ if (null === $value && \in_array($this->typeName, self::ALLOWED_UNION_TYPES, true)) {
+ return true;
+ }
+
+ if ('array' === $this->typeName && $this->allowNull && [] === $value) {
+ return null;
+ }
+
+ if ('bool' !== $this->typeName) {
+ return $value;
+ }
+
+ if ($this->allowNull && null === $value) {
+ return null;
+ }
+
+ return $value ?? $this->default;
+ }
+
+ private function handleUnion(\ReflectionUnionType $type): self
+ {
+ $types = array_map(
+ static fn (\ReflectionType $t) => $t instanceof \ReflectionNamedType ? $t->getName() : null,
+ $type->getTypes(),
+ );
+
+ sort($types);
+
+ $this->typeName = implode('|', array_filter($types));
+
+ if (!\in_array($this->typeName, self::ALLOWED_UNION_TYPES, true)) {
+ throw new LogicException(\sprintf('The union type for parameter "$%s" of "%s()" is not supported as a command option. Only "%s" types are allowed.', $this->name, $this->function, implode('", "', self::ALLOWED_UNION_TYPES)));
+ }
+
+ if (false !== $this->default) {
+ throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must have a default value of false.', $this->name, $this->function));
+ }
+
+ $this->mode = InputOption::VALUE_OPTIONAL;
+
+ return $this;
+ }
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ff71d2fa..9f3ae3d7d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,36 @@
CHANGELOG
=========
+7.3
+---
+
+ * Add `TreeHelper` and `TreeStyle` to display tree-like structures
+ * Add `SymfonyStyle::createTree()`
+ * Add support for invokable commands and add `#[Argument]` and `#[Option]` attributes to define input arguments and options
+ * Deprecate not declaring the parameter type in callable commands defined through `setCode` method
+ * Add support for help definition via `AsCommand` attribute
+ * Deprecate methods `Command::getDefaultName()` and `Command::getDefaultDescription()` in favor of the `#[AsCommand]` attribute
+ * Add support for Markdown format in `Table`
+ * Add support for `LockableTrait` in invokable commands
+ * Deprecate returning a non-integer value from a `\Closure` function set via `Command::setCode()`
+ * Mark `#[AsCommand]` attribute as `@final`
+ * Add support for `SignalableCommandInterface` with invokable commands
+
+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
---
diff --git a/CI/GithubActionReporter.php b/CI/GithubActionReporter.php
index 7e5565469..952d380d5 100644
--- a/CI/GithubActionReporter.php
+++ b/CI/GithubActionReporter.php
@@ -20,8 +20,6 @@
*/
class GithubActionReporter
{
- private OutputInterface $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
@@ -57,7 +55,7 @@ public static function isGithubActionEnvironment(): bool
*
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
*/
- public function error(string $message, string $file = null, int $line = null, int $col = null): void
+ public function error(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
{
$this->log('error', $message, $file, $line, $col);
}
@@ -67,7 +65,7 @@ public function error(string $message, string $file = null, int $line = null, in
*
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message
*/
- public function warning(string $message, string $file = null, int $line = null, int $col = null): void
+ public function warning(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
{
$this->log('warning', $message, $file, $line, $col);
}
@@ -77,23 +75,23 @@ public function warning(string $message, string $file = null, int $line = null,
*
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message
*/
- public function debug(string $message, string $file = null, int $line = null, int $col = null): void
+ public function debug(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
{
$this->log('debug', $message, $file, $line, $col);
}
- private function log(string $type, string $message, string $file = null, int $line = null, int $col = null): void
+ private function log(string $type, string $message, ?string $file = null, ?int $line = null, ?int $col = null): void
{
// Some values must be encoded.
$message = strtr($message, self::ESCAPED_DATA);
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 60ed046a6..b1914c19a 100644
--- a/Color.php
+++ b/Color.php
@@ -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
@@ -128,6 +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)))));
+ 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 c49891777..72a10cf76 100644
--- a/Command/Command.php
+++ b/Command/Command.php
@@ -32,7 +32,7 @@
*
* @author Fabien Potencier
*/
-class Command
+class Command implements SignalableCommandInterface
{
// see https://tldp.org/LDP/abs/html/exitcodes.html
public const SUCCESS = 0;
@@ -49,13 +49,18 @@ class Command
private string $description = '';
private ?InputDefinition $fullDefinition = null;
private bool $ignoreValidationErrors = false;
- private ?\Closure $code = null;
+ private ?InvokableCommand $code = null;
private array $synopsis = [];
private array $usages = [];
private ?HelperSet $helperSet = null;
+ /**
+ * @deprecated since Symfony 7.3, use the #[AsCommand] attribute instead
+ */
public static function getDefaultName(): ?string
{
+ trigger_deprecation('symfony/console', '7.3', 'Method "%s()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', __METHOD__);
+
if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) {
return $attribute[0]->newInstance()->name;
}
@@ -63,8 +68,13 @@ public static function getDefaultName(): ?string
return null;
}
+ /**
+ * @deprecated since Symfony 7.3, use the #[AsCommand] attribute instead
+ */
public static function getDefaultDescription(): ?string
{
+ trigger_deprecation('symfony/console', '7.3', 'Method "%s()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', __METHOD__);
+
if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) {
return $attribute[0]->newInstance()->description;
}
@@ -77,11 +87,23 @@ public static function getDefaultDescription(): ?string
*
* @throws LogicException When the command name is empty
*/
- public function __construct(string $name = null)
+ public function __construct(?string $name = null)
{
$this->definition = new InputDefinition();
- if (null === $name && null !== $name = static::getDefaultName()) {
+ $attribute = ((new \ReflectionClass(static::class))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance();
+
+ if (null === $name) {
+ if (self::class !== (new \ReflectionMethod($this, 'getDefaultName'))->class) {
+ trigger_deprecation('symfony/console', '7.3', 'Overriding "Command::getDefaultName()" in "%s" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', static::class);
+
+ $defaultName = static::getDefaultName();
+ } else {
+ $defaultName = $attribute?->name;
+ }
+ }
+
+ if (null === $name && null !== $name = $defaultName) {
$aliases = explode('|', $name);
if ('' === $name = array_shift($aliases)) {
@@ -97,7 +119,23 @@ public function __construct(string $name = null)
}
if ('' === $this->description) {
- $this->setDescription(static::getDefaultDescription() ?? '');
+ if (self::class !== (new \ReflectionMethod($this, 'getDefaultDescription'))->class) {
+ trigger_deprecation('symfony/console', '7.3', 'Overriding "Command::getDefaultDescription()" in "%s" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', static::class);
+
+ $defaultDescription = static::getDefaultDescription();
+ } else {
+ $defaultDescription = $attribute?->description;
+ }
+
+ $this->setDescription($defaultDescription ?? '');
+ }
+
+ if ('' === $this->help) {
+ $this->setHelp($attribute?->help ?? '');
+ }
+
+ if (\is_callable($this) && self::class === (new \ReflectionMethod($this, 'execute'))->getDeclaringClass()->name) {
+ $this->code = new InvokableCommand($this, $this(...));
}
$this->configure();
@@ -274,20 +312,14 @@ public function run(InputInterface $input, OutputInterface $output): int
$input->validate();
if ($this->code) {
- $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 ($this->code)($input, $output);
}
- return is_numeric($statusCode) ? (int) $statusCode : 0;
+ return $this->execute($input, $output);
}
/**
- * 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
{
@@ -315,23 +347,7 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti
*/
public function setCode(callable $code): static
{
- if ($code instanceof \Closure) {
- $r = new \ReflectionFunction($code);
- if (null === $r->getClosureThis()) {
- set_error_handler(static function () {});
- try {
- if ($c = \Closure::bind($code, $this)) {
- $code = $c;
- }
- } finally {
- restore_error_handler();
- }
- }
- } else {
- $code = $code(...);
- }
-
- $this->code = $code;
+ $this->code = new InvokableCommand($this, $code);
return $this;
}
@@ -399,21 +415,27 @@ public function getDefinition(): InputDefinition
*/
public function getNativeDefinition(): InputDefinition
{
- return $this->definition ?? throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
+ $definition = $this->definition ?? throw new LogicException(\sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
+
+ if ($this->code && !$definition->getArguments() && !$definition->getOptions()) {
+ $this->code->configure($definition);
+ }
+
+ return $definition;
}
/**
* Adds an argument.
*
- * @param $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
- * @param $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 = '', mixed $default = null, array|\Closure $suggestedValues = []): static
+ 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, $suggestedValues));
$this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
@@ -424,16 +446,16 @@ public function addArgument(string $name, int $mode = null, string $description
/**
* Adds an option.
*
- * @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 $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, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
+ 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, $suggestedValues));
$this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
@@ -607,7 +629,7 @@ 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];
@@ -621,7 +643,7 @@ public function getSynopsis(bool $short = false): string
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;
@@ -646,12 +668,22 @@ public function getUsages(): array
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);
}
+ public function getSubscribedSignals(): array
+ {
+ return $this->code?->getSubscribedSignals() ?? [];
+ }
+
+ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
+ {
+ return $this->code?->handleSignal($signal, $previousExitCode) ?? false;
+ }
+
/**
* Validates a command name.
*
@@ -662,7 +694,7 @@ public function getHelper(string $name): HelperInterface
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 38aa737f0..15eeea16a 100644
--- a/Command/CompleteCommand.php
+++ b/Command/CompleteCommand.php
@@ -74,7 +74,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
// "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);
+ $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.');
@@ -88,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);
@@ -98,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.');
@@ -185,7 +185,7 @@ private function createCompletionInput(InputInterface $input): CompletionInput
return $completionInput;
}
- private function findCommand(CompletionInput $completionInput, OutputInterface $output): ?Command
+ private function findCommand(CompletionInput $completionInput): ?Command
{
try {
$inputName = $completionInput->getFirstArgument();
diff --git a/Command/DumpCompletionCommand.php b/Command/DumpCompletionCommand.php
index be6f54592..2853fc5f4 100644
--- a/Command/DumpCompletionCommand.php
+++ b/Command/DumpCompletionCommand.php
@@ -35,7 +35,7 @@ protected function configure(): void
$commandName = basename($fullCommand);
$fullCommand = @realpath($fullCommand) ?: $fullCommand;
- $shell = $this->guessShell();
+ $shell = self::guessShell();
[$rcFile, $completionFile] = match ($shell) {
'fish' => ['~/.config/fish/config.fish', "/etc/fish/completions/$commandName.fish"],
'zsh' => ['~/.zshrc', '$fpath[1]/_'.$commandName],
@@ -98,9 +98,9 @@ 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;
diff --git a/Command/InvokableCommand.php b/Command/InvokableCommand.php
new file mode 100644
index 000000000..72ff407c8
--- /dev/null
+++ b/Command/InvokableCommand.php
@@ -0,0 +1,157 @@
+
+ *
+ * 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\Attribute\Argument;
+use Symfony\Component\Console\Attribute\Option;
+use Symfony\Component\Console\Exception\LogicException;
+use Symfony\Component\Console\Exception\RuntimeException;
+use Symfony\Component\Console\Input\InputDefinition;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+/**
+ * Represents an invokable command.
+ *
+ * @author Yonel Ceruto
+ *
+ * @internal
+ */
+class InvokableCommand implements SignalableCommandInterface
+{
+ private readonly \Closure $code;
+ private readonly ?SignalableCommandInterface $signalableCommand;
+ private readonly \ReflectionFunction $reflection;
+ private bool $triggerDeprecations = false;
+
+ public function __construct(
+ private readonly Command $command,
+ callable $code,
+ ) {
+ $this->code = $this->getClosure($code);
+ $this->signalableCommand = $code instanceof SignalableCommandInterface ? $code : null;
+ $this->reflection = new \ReflectionFunction($this->code);
+ }
+
+ /**
+ * Invokes a callable with parameters generated from the input interface.
+ */
+ public function __invoke(InputInterface $input, OutputInterface $output): int
+ {
+ $statusCode = ($this->code)(...$this->getParameters($input, $output));
+
+ if (!\is_int($statusCode)) {
+ if ($this->triggerDeprecations) {
+ trigger_deprecation('symfony/console', '7.3', \sprintf('Returning a non-integer value from the command "%s" is deprecated and will throw an exception in Symfony 8.0.', $this->command->getName()));
+
+ return 0;
+ }
+
+ throw new \TypeError(\sprintf('The command "%s" must return an integer value in the "%s" method, but "%s" was returned.', $this->command->getName(), $this->reflection->getName(), get_debug_type($statusCode)));
+ }
+
+ return $statusCode;
+ }
+
+ /**
+ * Configures the input definition from an invokable-defined function.
+ *
+ * Processes the parameters of the reflection function to extract and
+ * add arguments or options to the provided input definition.
+ */
+ public function configure(InputDefinition $definition): void
+ {
+ foreach ($this->reflection->getParameters() as $parameter) {
+ if ($argument = Argument::tryFrom($parameter)) {
+ $definition->addArgument($argument->toInputArgument());
+ } elseif ($option = Option::tryFrom($parameter)) {
+ $definition->addOption($option->toInputOption());
+ }
+ }
+ }
+
+ private function getClosure(callable $code): \Closure
+ {
+ if (!$code instanceof \Closure) {
+ return $code(...);
+ }
+
+ $this->triggerDeprecations = true;
+
+ if (null !== (new \ReflectionFunction($code))->getClosureThis()) {
+ return $code;
+ }
+
+ set_error_handler(static function () {});
+ try {
+ if ($c = \Closure::bind($code, $this->command)) {
+ $code = $c;
+ }
+ } finally {
+ restore_error_handler();
+ }
+
+ return $code;
+ }
+
+ private function getParameters(InputInterface $input, OutputInterface $output): array
+ {
+ $parameters = [];
+ foreach ($this->reflection->getParameters() as $parameter) {
+ if ($argument = Argument::tryFrom($parameter)) {
+ $parameters[] = $argument->resolveValue($input);
+
+ continue;
+ }
+
+ if ($option = Option::tryFrom($parameter)) {
+ $parameters[] = $option->resolveValue($input);
+
+ continue;
+ }
+
+ $type = $parameter->getType();
+
+ if (!$type instanceof \ReflectionNamedType) {
+ if ($this->triggerDeprecations) {
+ trigger_deprecation('symfony/console', '7.3', \sprintf('Omitting the type declaration for the parameter "$%s" is deprecated and will throw an exception in Symfony 8.0.', $parameter->getName()));
+
+ continue;
+ }
+
+ throw new LogicException(\sprintf('The parameter "$%s" must have a named type. Untyped, Union or Intersection types are not supported.', $parameter->getName()));
+ }
+
+ $parameters[] = match ($type->getName()) {
+ InputInterface::class => $input,
+ OutputInterface::class => $output,
+ SymfonyStyle::class => new SymfonyStyle($input, $output),
+ Application::class => $this->command->getApplication(),
+ default => throw new RuntimeException(\sprintf('Unsupported type "%s" for parameter "$%s".', $type->getName(), $parameter->getName())),
+ };
+ }
+
+ return $parameters ?: [$input, $output];
+ }
+
+ public function getSubscribedSignals(): array
+ {
+ return $this->signalableCommand?->getSubscribedSignals() ?? [];
+ }
+
+ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
+ {
+ return $this->signalableCommand?->handleSignal($signal, $previousExitCode) ?? false;
+ }
+}
diff --git a/Command/LazyCommand.php b/Command/LazyCommand.php
index 7279724a6..fd2c300d7 100644
--- a/Command/LazyCommand.php
+++ b/Command/LazyCommand.php
@@ -27,17 +27,21 @@
final class LazyCommand extends Command
{
private \Closure|Command $command;
- private ?bool $isEnabled;
- public function __construct(string $name, array $aliases, string $description, bool $isHidden, \Closure $commandFactory, ?bool $isEnabled = true)
- {
+ 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
@@ -113,7 +117,7 @@ public function getNativeDefinition(): InputDefinition
/**
* @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion
*/
- public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
+ public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
{
$this->getCommand()->addArgument($name, $mode, $description, $default, $suggestedValues);
@@ -123,7 +127,7 @@ public function addArgument(string $name, int $mode = null, string $description
/**
* @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion
*/
- public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
+ 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, $suggestedValues);
diff --git a/Command/LockableTrait.php b/Command/LockableTrait.php
index c1006a65c..b7abd2fdc 100644
--- a/Command/LockableTrait.php
+++ b/Command/LockableTrait.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Console\Command;
+use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\LockInterface;
@@ -26,10 +27,12 @@ trait LockableTrait
{
private ?LockInterface $lock = null;
+ private ?LockFactory $lockFactory = null;
+
/**
* Locks a command.
*/
- private function lock(string $name = null, bool $blocking = false): bool
+ 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. Try running "composer require symfony/lock".');
@@ -39,13 +42,27 @@ private function lock(string $name = null, bool $blocking = false): bool
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);
+ }
+
+ if (!$name) {
+ if ($this instanceof Command) {
+ $name = $this->getName();
+ } elseif ($attribute = (new \ReflectionClass($this::class))->getAttributes(AsCommand::class)) {
+ $name = $attribute[0]->newInstance()->name;
+ } else {
+ throw new LogicException(\sprintf('Lock name missing: provide it via "%s()", #[AsCommand] attribute, or by extending Command class.', __METHOD__));
+ }
}
- $this->lock = (new LockFactory($store))->createLock($name ?: $this->getName());
+ $this->lock = $this->lockFactory->createLock($name);
if (!$this->lock->acquire($blocking)) {
$this->lock = null;
diff --git a/Command/TraceableCommand.php b/Command/TraceableCommand.php
index d8c46b7fa..ed11cc29f 100644
--- a/Command/TraceableCommand.php
+++ b/Command/TraceableCommand.php
@@ -27,7 +27,7 @@
*
* @author Jules Pietri
*/
-final class TraceableCommand extends Command implements SignalableCommandInterface
+final class TraceableCommand extends Command
{
public readonly Command $command;
public int $exitCode;
@@ -45,6 +45,7 @@ final class TraceableCommand extends Command implements SignalableCommandInterfa
/** @var array */
public array $interactiveInputs = [];
public array $handledSignals = [];
+ public ?array $invokableCommandInfo = null;
public function __construct(
Command $command,
@@ -88,15 +89,11 @@ public function __call(string $name, array $arguments): mixed
public function getSubscribedSignals(): array
{
- return $this->command instanceof SignalableCommandInterface ? $this->command->getSubscribedSignals() : [];
+ return $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);
@@ -134,7 +131,7 @@ public function ignoreValidationErrors(): void
parent::ignoreValidationErrors();
}
- public function setApplication(Application $application = null): void
+ public function setApplication(?Application $application = null): void
{
$this->command->setApplication($application);
}
@@ -171,6 +168,18 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti
*/
public function setCode(callable $code): static
{
+ if ($code instanceof InvokableCommand) {
+ $r = new \ReflectionFunction(\Closure::bind(function () {
+ return $this->code;
+ }, $code, InvokableCommand::class)());
+
+ $this->invokableCommandInfo = [
+ 'class' => $r->getClosureScopeClass()->name,
+ 'file' => $r->getFileName(),
+ 'line' => $r->getStartLine(),
+ ];
+ }
+
$this->command->setCode($code);
return parent::setCode(function (InputInterface $input, OutputInterface $output) use ($code): int {
@@ -209,14 +218,14 @@ 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
+ 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 $shortcut = null, int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
+ 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);
@@ -283,7 +292,7 @@ public function run(InputInterface $input, OutputInterface $output): int
$event = $this->stopwatch->start($this->getName(), 'command');
try {
- $this->exitCode = parent::run($input, $output);
+ $this->exitCode = $this->command->run($input, $output);
} finally {
$event->stop();
diff --git a/CommandLoader/ContainerCommandLoader.php b/CommandLoader/ContainerCommandLoader.php
index bfa0ac467..eb4945135 100644
--- a/CommandLoader/ContainerCommandLoader.php
+++ b/CommandLoader/ContainerCommandLoader.php
@@ -22,22 +22,19 @@
*/
class ContainerCommandLoader implements CommandLoaderInterface
{
- private ContainerInterface $container;
- private array $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,
+ ) {
}
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]);
diff --git a/CommandLoader/FactoryCommandLoader.php b/CommandLoader/FactoryCommandLoader.php
index 9ced75aeb..2d13139c2 100644
--- a/CommandLoader/FactoryCommandLoader.php
+++ b/CommandLoader/FactoryCommandLoader.php
@@ -21,14 +21,12 @@
*/
class FactoryCommandLoader implements CommandLoaderInterface
{
- private array $factories;
-
/**
* @param callable[] $factories Indexed by command names
*/
- public function __construct(array $factories)
- {
- $this->factories = $factories;
+ public function __construct(
+ private array $factories,
+ ) {
}
public function has(string $name): bool
@@ -39,7 +37,7 @@ public function has(string $name): bool
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];
diff --git a/Completion/CompletionInput.php b/Completion/CompletionInput.php
index 7ba41c083..9f9619e18 100644
--- a/Completion/CompletionInput.php
+++ b/Completion/CompletionInput.php
@@ -53,7 +53,7 @@ public static function fromString(string $inputStr, int $currentIndex): self
* Create an input based on an COMP_WORDS token list.
*
* @param string[] $tokens the set of split tokens (e.g. COMP_WORDS or argv)
- * @param $currentIndex the index of the cursor (e.g. COMP_CWORD)
+ * @param int $currentIndex the index of the cursor (e.g. COMP_CWORD)
*/
public static function fromTokens(array $tokens, int $currentIndex): self
{
@@ -123,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 = '';
}
}
@@ -226,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/Output/FishCompletionOutput.php b/Completion/Output/FishCompletionOutput.php
index d2c414e48..356a974ea 100644
--- a/Completion/Output/FishCompletionOutput.php
+++ b/Completion/Output/FishCompletionOutput.php
@@ -21,11 +21,14 @@ class FishCompletionOutput implements CompletionOutputInterface
{
public function write(CompletionSuggestions $suggestions, OutputInterface $output): void
{
- $values = $suggestions->getValueSuggestions();
+ $values = [];
+ foreach ($suggestions->getValueSuggestions() as $value) {
+ $values[] = $value->getValue().($value->getDescription() ? "\t".$value->getDescription() : '');
+ }
foreach ($suggestions->getOptionSuggestions() as $option) {
- $values[] = '--'.$option->getName();
+ $values[] = '--'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
if ($option->isNegatable()) {
- $values[] = '--no-'.$option->getName();
+ $values[] = '--no-'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : '');
}
}
$output->write(implode("\n", $values));
diff --git a/Completion/Suggestion.php b/Completion/Suggestion.php
index 7392965a2..3251b079f 100644
--- a/Completion/Suggestion.php
+++ b/Completion/Suggestion.php
@@ -20,7 +20,7 @@ class Suggestion implements \Stringable
{
public function __construct(
private readonly string $value,
- private readonly string $description = ''
+ private readonly string $description = '',
) {
}
diff --git a/Cursor.php b/Cursor.php
index 69fd3821c..e2618cf1d 100644
--- a/Cursor.php
+++ b/Cursor.php
@@ -18,16 +18,16 @@
*/
final class Cursor
{
- private OutputInterface $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+'));
}
@@ -36,7 +36,7 @@ public function __construct(OutputInterface $output, $input = null)
*/
public function moveUp(int $lines = 1): static
{
- $this->output->write(sprintf("\x1b[%dA", $lines));
+ $this->output->write(\sprintf("\x1b[%dA", $lines));
return $this;
}
@@ -46,7 +46,7 @@ public function moveUp(int $lines = 1): static
*/
public function moveDown(int $lines = 1): static
{
- $this->output->write(sprintf("\x1b[%dB", $lines));
+ $this->output->write(\sprintf("\x1b[%dB", $lines));
return $this;
}
@@ -56,7 +56,7 @@ public function moveDown(int $lines = 1): static
*/
public function moveRight(int $columns = 1): static
{
- $this->output->write(sprintf("\x1b[%dC", $columns));
+ $this->output->write(\sprintf("\x1b[%dC", $columns));
return $this;
}
@@ -66,7 +66,7 @@ public function moveRight(int $columns = 1): static
*/
public function moveLeft(int $columns = 1): static
{
- $this->output->write(sprintf("\x1b[%dD", $columns));
+ $this->output->write(\sprintf("\x1b[%dD", $columns));
return $this;
}
@@ -76,7 +76,7 @@ public function moveLeft(int $columns = 1): static
*/
public function moveToColumn(int $column): static
{
- $this->output->write(sprintf("\x1b[%dG", $column));
+ $this->output->write(\sprintf("\x1b[%dG", $column));
return $this;
}
@@ -86,7 +86,7 @@ public function moveToColumn(int $column): static
*/
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;
}
@@ -195,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
index 16a0eadf4..6dcac66bb 100644
--- a/DataCollector/CommandDataCollector.php
+++ b/DataCollector/CommandDataCollector.php
@@ -27,7 +27,7 @@
*/
final class CommandDataCollector extends DataCollector
{
- public function collect(Request $request, Response $response, \Throwable $exception = null): void
+ public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
if (!$request instanceof CliRequest) {
return;
@@ -37,7 +37,7 @@ public function collect(Request $request, Response $response, \Throwable $except
$application = $command->getApplication();
$this->data = [
- 'command' => $this->cloneVar($command->command),
+ 'command' => $command->invokableCommandInfo ?? $this->cloneVar($command->command),
'exit_code' => $command->exitCode,
'interrupted_by_signal' => $command->interruptedBySignal,
'duration' => $command->duration,
@@ -95,6 +95,10 @@ public function getName(): string
*/
public function getCommand(): array
{
+ if (\is_array($this->data['command'])) {
+ return $this->data['command'];
+ }
+
$class = $this->data['command']->getType();
$r = new \ReflectionMethod($class, 'execute');
@@ -118,7 +122,7 @@ public function getCommand(): array
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 \sprintf('%s (%d)', SignalMap::getSignalName($this->data['interrupted_by_signal']), $this->data['interrupted_by_signal']);
}
return null;
@@ -204,7 +208,7 @@ public function getInteractiveInputs(): array
public function getSignalable(): array
{
return array_map(
- static fn (int $signal): string => sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal),
+ static fn (int $signal): string => \sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal),
$this->data['signalable']
);
}
@@ -212,7 +216,7 @@ public function getSignalable(): array
public function getHandledSignals(): array
{
$keys = array_map(
- static fn (int $signal): string => sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal),
+ static fn (int $signal): string => \sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal),
array_keys($this->data['handled_signals'])
);
diff --git a/DependencyInjection/AddConsoleCommandPass.php b/DependencyInjection/AddConsoleCommandPass.php
index f712c614a..562627f4b 100644
--- a/DependencyInjection/AddConsoleCommandPass.php
+++ b/DependencyInjection/AddConsoleCommandPass.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Console\DependencyInjection;
+use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\LazyCommand;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
@@ -38,22 +39,39 @@ public function process(ContainerBuilder $container): void
foreach ($commandServices as $id => $tags) {
$definition = $container->getDefinition($id);
- $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));
- }
- if (!$r->isSubclassOf(Command::class)) {
- throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class));
+ if (!$r = $container->getReflectionClass($class)) {
+ throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
+ }
+
+ if (!$r->isSubclassOf(Command::class)) {
+ if (!$r->hasMethod('__invoke')) {
+ throw new InvalidArgumentException(\sprintf('The service "%s" tagged "%s" must either be a subclass of "%s" or have an "__invoke()" method.', $id, 'console.command', Command::class));
}
- $aliases = str_replace('%', '%%', $class::getDefaultName() ?? '');
+
+ $invokableRef = new Reference($id);
+ $definition = $container->register($id .= '.command', $class = Command::class)
+ ->addMethodCall('setCode', [$invokableRef]);
+ } else {
+ $invokableRef = null;
+ }
+
+ $definition->addTag('container.no_preload');
+
+ /** @var AsCommand|null $attribute */
+ $attribute = ($r->getAttributes(AsCommand::class)[0] ?? null)?->newInstance();
+
+ if (Command::class !== (new \ReflectionMethod($class, 'getDefaultName'))->class) {
+ trigger_deprecation('symfony/console', '7.3', 'Overriding "Command::getDefaultName()" in "%s" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', $class);
+
+ $defaultName = $class::getDefaultName();
+ } else {
+ $defaultName = $attribute?->name;
}
- $aliases = explode('|', $aliases ?? '');
+ $aliases = str_replace('%', '%%', $tags[0]['command'] ?? $defaultName ?? '');
+ $aliases = explode('|', $aliases);
$commandName = array_shift($aliases);
if ($isHidden = '' === $commandName) {
@@ -72,6 +90,7 @@ public function process(ContainerBuilder $container): void
}
$description = $tags[0]['description'] ?? null;
+ $help = $tags[0]['help'] ?? null;
unset($tags[0]);
$lazyCommandMap[$commandName] = $id;
@@ -88,6 +107,7 @@ public function process(ContainerBuilder $container): void
}
$description ??= $tag['description'] ?? null;
+ $help ??= $tag['help'] ?? null;
}
$definition->addMethodCall('setName', [$commandName]);
@@ -100,18 +120,22 @@ public function process(ContainerBuilder $container): void
$definition->addMethodCall('setHidden', [true]);
}
+ if ($help && $invokableRef) {
+ $definition->addMethodCall('setHelp', [str_replace('%', '%%', $help)]);
+ }
+
if (!$description) {
- if (!$r = $container->getReflectionClass($class)) {
- 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, 'console.command', Command::class));
+ if (Command::class !== (new \ReflectionMethod($class, 'getDefaultDescription'))->class) {
+ trigger_deprecation('symfony/console', '7.3', 'Overriding "Command::getDefaultDescription()" in "%s" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', $class);
+
+ $description = $class::getDefaultDescription();
+ } else {
+ $description = $attribute?->description;
}
- $description = str_replace('%', '%%', $class::getDefaultDescription() ?? '');
}
if ($description) {
- $definition->addMethodCall('setDescription', [$description]);
+ $definition->addMethodCall('setDescription', [str_replace('%', '%%', $description)]);
$container->register('.'.$id.'.lazy', LazyCommand::class)
->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]);
diff --git a/Descriptor/ApplicationDescription.php b/Descriptor/ApplicationDescription.php
index f8ed18045..802d68560 100644
--- a/Descriptor/ApplicationDescription.php
+++ b/Descriptor/ApplicationDescription.php
@@ -24,9 +24,6 @@ class ApplicationDescription
{
public const GLOBAL_NAMESPACE = '_global';
- private Application $application;
- private ?string $namespace;
- private bool $showHidden;
private array $namespaces;
/**
@@ -39,11 +36,11 @@ class ApplicationDescription
*/
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
@@ -73,7 +70,7 @@ 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];
diff --git a/Descriptor/Descriptor.php b/Descriptor/Descriptor.php
index 7b2509c60..2143a17c3 100644
--- a/Descriptor/Descriptor.php
+++ b/Descriptor/Descriptor.php
@@ -38,7 +38,7 @@ public function describe(OutputInterface $output, object $object, array $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))),
+ default => throw new InvalidArgumentException(\sprintf('Object of type "%s" is not describable.', get_debug_type($object))),
};
}
diff --git a/Descriptor/JsonDescriptor.php b/Descriptor/JsonDescriptor.php
index 956303709..9a8e696cd 100644
--- a/Descriptor/JsonDescriptor.php
+++ b/Descriptor/JsonDescriptor.php
@@ -108,7 +108,7 @@ private function getInputOptionData(InputOption $option, bool $negated = false):
'is_value_required' => false,
'is_multiple' => false,
'description' => 'Negate the "--'.$option->getName().'" option',
- 'default' => false,
+ 'default' => null === $option->getDefault() ? null : !$option->getDefault(),
] : [
'name' => '--'.$option->getName(),
'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '',
diff --git a/Descriptor/MarkdownDescriptor.php b/Descriptor/MarkdownDescriptor.php
index b3f16ee90..8b7075943 100644
--- a/Descriptor/MarkdownDescriptor.php
+++ b/Descriptor/MarkdownDescriptor.php
@@ -149,7 +149,7 @@ protected function describeApplication(Application $application, array $options
}
$this->write("\n\n");
- $this->write(implode("\n", array_map(fn ($commandName) => 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) {
@@ -162,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
index d4423fd34..d2dde6fba 100644
--- a/Descriptor/ReStructuredTextDescriptor.php
+++ b/Descriptor/ReStructuredTextDescriptor.php
@@ -92,7 +92,7 @@ protected function describeInputOption(InputOption $option, array $options = [])
protected function describeInputDefinition(InputDefinition $definition, array $options = []): void
{
if ($showArguments = ((bool) $definition->getArguments())) {
- $this->write("Arguments\n".str_repeat($this->subsubsectionChar, 9))."\n\n";
+ $this->write("Arguments\n".str_repeat($this->subsubsectionChar, 9));
foreach ($definition->getArguments() as $argument) {
$this->write("\n\n");
$this->describeInputArgument($argument);
@@ -167,7 +167,7 @@ private function getApplicationTitle(Application $application): string
return 'Console Tool';
}
if ('UNKNOWN' !== $application->getVersion()) {
- return sprintf('%s %s', $application->getName(), $application->getVersion());
+ return \sprintf('%s %s', $application->getName(), $application->getVersion());
}
return $application->getName();
@@ -209,7 +209,7 @@ private function createTableOfContents(ApplicationDescription $description, Appl
$commands = $this->removeAliasesAndHiddenCommands($commands);
$this->write("\n\n");
- $this->write(implode("\n", array_map(static fn ($commandName) => sprintf('- `%s`_', $commandName), array_keys($commands))));
+ $this->write(implode("\n", array_map(static fn ($commandName) => \sprintf('- `%s`_', $commandName), array_keys($commands))));
}
}
@@ -217,6 +217,7 @@ private function getNonDefaultOptions(InputDefinition $definition): array
{
$globalOptions = [
'help',
+ 'silent',
'quiet',
'verbose',
'version',
@@ -226,7 +227,7 @@ private function getNonDefaultOptions(InputDefinition $definition): array
$nonDefaultOptions = [];
foreach ($definition->getOptions() as $option) {
// Skip global options.
- if (!\in_array($option->getName(), $globalOptions)) {
+ if (!\in_array($option->getName(), $globalOptions, true)) {
$nonDefaultOptions[] = $option;
}
}
diff --git a/Descriptor/TextDescriptor.php b/Descriptor/TextDescriptor.php
index d04d10238..51c411f46 100644
--- a/Descriptor/TextDescriptor.php
+++ b/Descriptor/TextDescriptor.php
@@ -31,7 +31,7 @@ class TextDescriptor extends Descriptor
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 = '';
}
@@ -39,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,7 +51,7 @@ protected function describeInputArgument(InputArgument $argument, 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 = '';
}
@@ -66,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
@@ -166,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 {
@@ -196,7 +196,7 @@ protected function describeApplication(Application $application, array $options
$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);
}
@@ -218,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);
}
}
diff --git a/Descriptor/XmlDescriptor.php b/Descriptor/XmlDescriptor.php
index 72580fd98..00055557c 100644
--- a/Descriptor/XmlDescriptor.php
+++ b/Descriptor/XmlDescriptor.php
@@ -79,7 +79,7 @@ public function getCommandDocument(Command $command, bool $short = false): \DOMD
return $dom;
}
- public function getApplicationDocument(Application $application, string $namespace = null, bool $short = false): \DOMDocument
+ public function getApplicationDocument(Application $application, ?string $namespace = null, bool $short = false): \DOMDocument
{
$dom = new \DOMDocument('1.0', 'UTF-8');
$dom->appendChild($rootXml = $dom->createElement('symfony'));
@@ -208,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/ConsoleErrorEvent.php b/Event/ConsoleErrorEvent.php
index d4a691216..1c0d62652 100644
--- a/Event/ConsoleErrorEvent.php
+++ b/Event/ConsoleErrorEvent.php
@@ -22,14 +22,15 @@
*/
final class ConsoleErrorEvent extends ConsoleEvent
{
- private \Throwable $error;
private int $exitCode;
- public function __construct(InputInterface $input, OutputInterface $output, \Throwable $error, Command $command = null)
- {
+ 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
diff --git a/Event/ConsoleEvent.php b/Event/ConsoleEvent.php
index 437a58e1e..2f9f0778e 100644
--- a/Event/ConsoleEvent.php
+++ b/Event/ConsoleEvent.php
@@ -23,16 +23,11 @@
*/
class ConsoleEvent extends Event
{
- protected ?Command $command;
-
- private InputInterface $input;
- private OutputInterface $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,
+ ) {
}
/**
diff --git a/Event/ConsoleSignalEvent.php b/Event/ConsoleSignalEvent.php
index 95af1f915..b27f08a18 100644
--- a/Event/ConsoleSignalEvent.php
+++ b/Event/ConsoleSignalEvent.php
@@ -20,14 +20,14 @@
*/
final class ConsoleSignalEvent extends ConsoleEvent
{
- private int $handlingSignal;
- private int|false $exitCode;
-
- public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal, int|false $exitCode = 0)
- {
+ 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;
- $this->exitCode = $exitCode;
}
public function getHandlingSignal(): int
diff --git a/EventListener/ErrorListener.php b/EventListener/ErrorListener.php
index 5c38e8ef8..9acb0e41d 100644
--- a/EventListener/ErrorListener.php
+++ b/EventListener/ErrorListener.php
@@ -24,11 +24,9 @@
*/
class ErrorListener implements EventSubscriberInterface
{
- private ?LoggerInterface $logger;
-
- public function __construct(LoggerInterface $logger = null)
- {
- $this->logger = $logger;
+ public function __construct(
+ private ?LoggerInterface $logger = null,
+ ) {
}
public function onConsoleError(ConsoleErrorEvent $event): void
@@ -39,7 +37,7 @@ public function onConsoleError(ConsoleErrorEvent $event): void
$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;
@@ -60,7 +58,7 @@ public function onConsoleTerminate(ConsoleTerminateEvent $event): void
return;
}
- if (!$inputString = $this->getInputString($event)) {
+ if (!$inputString = self::getInputString($event)) {
$this->logger->debug('The console exited with code "{code}"', ['code' => $exitCode]);
return;
@@ -77,19 +75,15 @@ public static function getSubscribedEvents(): array
];
}
- private static function getInputString(ConsoleEvent $event): ?string
+ private static function getInputString(ConsoleEvent $event): string
{
$commandName = $event->getCommand()?->getName();
- $input = $event->getInput();
-
- if ($input instanceof \Stringable) {
- if ($commandName) {
- return str_replace(["'$commandName'", "\"$commandName\""], $commandName, (string) $input);
- }
+ $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 1e9f1c793..246f04fa2 100644
--- a/Exception/CommandNotFoundException.php
+++ b/Exception/CommandNotFoundException.php
@@ -18,19 +18,19 @@
*/
class CommandNotFoundException extends \InvalidArgumentException implements ExceptionInterface
{
- private array $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;
}
/**
diff --git a/Formatter/OutputFormatter.php b/Formatter/OutputFormatter.php
index 8e81e5900..c72728b27 100644
--- a/Formatter/OutputFormatter.php
+++ b/Formatter/OutputFormatter.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Console\Formatter;
use Symfony\Component\Console\Exception\InvalidArgumentException;
+use Symfony\Component\Console\Helper\Helper;
use function Symfony\Component\String\b;
@@ -23,7 +24,6 @@
*/
class OutputFormatter implements WrappableOutputFormatterInterface
{
- private bool $decorated;
private array $styles = [];
private OutputFormatterStyleStack $styleStack;
@@ -67,10 +67,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'));
@@ -106,7 +106,7 @@ public function hasStyle(string $name): bool
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)];
@@ -137,9 +137,11 @@ public function formatAndWrap(?string $message, int $width): string
continue;
}
+ // convert byte position to character position.
+ $pos = Helper::length(substr($message, 0, $pos));
// add the text up to the next tag
- $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
- $offset = $pos + \strlen($text);
+ $output .= $this->applyCurrentStyle(Helper::substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength);
+ $offset = $pos + Helper::length($text);
// opening tag?
if ($open = '/' !== $text[1]) {
@@ -160,7 +162,7 @@ public function formatAndWrap(?string $message, int $width): string
}
}
- $output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength);
+ $output .= $this->applyCurrentStyle(Helper::substr($message, $offset), $output, $width, $currentLineLength);
return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']);
}
@@ -227,8 +229,18 @@ private function applyCurrentStyle(string $text, string $current, int $width, in
}
if ($currentLineLength) {
- $prefix = substr($text, 0, $i = $width - $currentLineLength)."\n";
- $text = substr($text, $i);
+ $lines = explode("\n", $text, 2);
+ $prefix = Helper::substr($lines[0], 0, $i = $width - $currentLineLength)."\n";
+ $text = Helper::substr($lines[0], $i);
+
+ if (isset($lines[1])) {
+ // $prefix may contain the full first line in which the \n is already a part of $prefix.
+ if ('' !== $text) {
+ $text .= "\n";
+ }
+
+ $text .= $lines[1];
+ }
} else {
$prefix = '';
}
@@ -243,8 +255,8 @@ private function applyCurrentStyle(string $text, string $current, int $width, in
$lines = explode("\n", $text);
- foreach ($lines as $line) {
- $currentLineLength += \strlen($line);
+ foreach ($lines as $i => $line) {
+ $currentLineLength = 0 === $i ? $currentLineLength + Helper::length($line) : Helper::length($line);
if ($width <= $currentLineLength) {
$currentLineLength = 0;
}
@@ -263,6 +275,6 @@ private function addLineBreaks(string $text, int $width): string
{
$encoding = mb_detect_encoding($text, null, true) ?: 'UTF-8';
- return b($text)->toCodePointString($encoding)->wordwrap($width, "\n", true)->toByteString($encoding);
+ return b($text)->toUnicodeString($encoding)->wordwrap($width, "\n", true)->toByteString($encoding);
}
}
diff --git a/Formatter/OutputFormatterStyle.php b/Formatter/OutputFormatterStyle.php
index 4582ccd05..20a65b517 100644
--- a/Formatter/OutputFormatterStyle.php
+++ b/Formatter/OutputFormatterStyle.php
@@ -33,7 +33,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
* @param string|null $foreground The style foreground color name
* @param string|null $background The style background color name
*/
- public function __construct(string $foreground = null, string $background = null, array $options = [])
+ public function __construct(?string $foreground = null, ?string $background = null, array $options = [])
{
$this->color = new Color($this->foreground = $foreground ?: '', $this->background = $background ?: '', $this->options = $options);
}
diff --git a/Formatter/OutputFormatterStyleStack.php b/Formatter/OutputFormatterStyleStack.php
index c3726a35d..4985213ab 100644
--- a/Formatter/OutputFormatterStyleStack.php
+++ b/Formatter/OutputFormatterStyleStack.php
@@ -26,7 +26,7 @@ class OutputFormatterStyleStack implements ResetInterface
private OutputFormatterStyleInterface $emptyStyle;
- public function __construct(OutputFormatterStyleInterface $emptyStyle = null)
+ public function __construct(?OutputFormatterStyleInterface $emptyStyle = null)
{
$this->emptyStyle = $emptyStyle ?? new OutputFormatterStyle();
$this->reset();
@@ -53,7 +53,7 @@ public function push(OutputFormatterStyleInterface $style): void
*
* @throws InvalidArgumentException When style tags incorrectly nested
*/
- public function pop(OutputFormatterStyleInterface $style = null): OutputFormatterStyleInterface
+ public function pop(?OutputFormatterStyleInterface $style = null): OutputFormatterStyleInterface
{
if (!$this->styles) {
return $this->emptyStyle;
diff --git a/Helper/DebugFormatterHelper.php b/Helper/DebugFormatterHelper.php
index 9ea7fb914..dfdb8a82c 100644
--- a/Helper/DebugFormatterHelper.php
+++ b/Helper/DebugFormatterHelper.php
@@ -31,7 +31,7 @@ public function start(string $id, string $message, string $prefix = 'RUN'): stri
{
$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);
}
/**
@@ -47,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;
@@ -76,10 +76,10 @@ public function stop(string $id, string $message, bool $successful, string $pref
$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']);
@@ -88,7 +88,7 @@ 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']]);
}
public function getName(): string
diff --git a/Helper/DescriptorHelper.php b/Helper/DescriptorHelper.php
index 300c7b102..9422271fb 100644
--- a/Helper/DescriptorHelper.php
+++ b/Helper/DescriptorHelper.php
@@ -60,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']];
diff --git a/Helper/Dumper.php b/Helper/Dumper.php
index 8c6a94d51..0cd01e616 100644
--- a/Helper/Dumper.php
+++ b/Helper/Dumper.php
@@ -21,17 +21,13 @@
*/
final class Dumper
{
- private OutputInterface $output;
- private ?CliDumper $dumper;
- private ?ClonerInterface $cloner;
private \Closure $handler;
- public function __construct(OutputInterface $output, CliDumper $dumper = null, ClonerInterface $cloner = null)
- {
- $this->output = $output;
- $this->dumper = $dumper;
- $this->cloner = $cloner;
-
+ 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 ??= new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR);
diff --git a/Helper/FormatterHelper.php b/Helper/FormatterHelper.php
index 279e4c799..3646b3d6f 100644
--- a/Helper/FormatterHelper.php
+++ b/Helper/FormatterHelper.php
@@ -25,7 +25,7 @@ class FormatterHelper extends Helper
*/
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);
}
/**
@@ -41,7 +41,7 @@ public function formatBlock(string|array $messages, string $style, bool $large =
$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);
}
@@ -54,7 +54,7 @@ public function formatBlock(string|array $messages, string $style, bool $large =
}
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);
diff --git a/Helper/Helper.php b/Helper/Helper.php
index afb20ad50..46e7e2f58 100644
--- a/Helper/Helper.php
+++ b/Helper/Helper.php
@@ -42,7 +42,9 @@ public static function width(?string $string): int
$string ??= '';
if (preg_match('//u', $string)) {
- return (new UnicodeString($string))->width(false);
+ $string = preg_replace('/[\p{Cc}\x7F]++/u', '', $string, -1, $count);
+
+ return (new UnicodeString($string))->width(false) + $count;
}
if (false === $encoding = mb_detect_encoding($string, null, true)) {
@@ -74,10 +76,14 @@ public static function length(?string $string): int
/**
* Returns the subset of a string, using mb_substr if it is available.
*/
- public static function substr(?string $string, int $from, int $length = null): string
+ public static function substr(?string $string, int $from, ?int $length = null): string
{
$string ??= '';
+ if (preg_match('//u', $string)) {
+ return (new UnicodeString($string))->slice($from, $length);
+ }
+
if (false === $encoding = mb_detect_encoding($string, null, true)) {
return substr($string, $from, $length);
}
@@ -87,39 +93,41 @@ public static function substr(?string $string, int $from, int $length = null): s
public static function formatTime(int|float $secs, int $precision = 1): string
{
+ $ms = (int) ($secs * 1000);
$secs = (int) floor($secs);
- if (0 === $secs) {
- return '< 1 sec';
+ if (0 === $ms) {
+ return '< 1 ms';
}
static $timeFormats = [
- [1, '1 sec', 'secs'],
- [60, '1 min', 'mins'],
- [3600, '1 hr', 'hrs'],
- [86400, '1 day', 'days'],
+ [1, 'ms'],
+ [1000, 's'],
+ [60000, 'min'],
+ [3600000, 'h'],
+ [86_400_000, 'd'],
];
$times = [];
foreach ($timeFormats as $index => $format) {
- $seconds = isset($timeFormats[$index + 1]) ? $secs % $timeFormats[$index + 1][0] : $secs;
+ $milliSeconds = isset($timeFormats[$index + 1]) ? $ms % $timeFormats[$index + 1][0] : $ms;
if (isset($times[$index - $precision])) {
unset($times[$index - $precision]);
}
- if (0 === $seconds) {
+ if (0 === $milliSeconds) {
continue;
}
- $unitCount = ($seconds / $format[0]);
- $times[$index] = 1 === $unitCount ? $format[1] : $unitCount.' '.$format[2];
+ $unitCount = ($milliSeconds / $format[0]);
+ $times[$index] = $unitCount.' '.$format[1];
- if ($secs === $seconds) {
+ if ($ms === $milliSeconds) {
break;
}
- $secs -= $seconds;
+ $ms -= $milliSeconds;
}
return implode(', ', array_reverse($times));
@@ -128,18 +136,18 @@ public static function formatTime(int|float $secs, int $precision = 1): string
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);
+ return \sprintf('%d B', $memory);
}
public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string): string
diff --git a/Helper/HelperSet.php b/Helper/HelperSet.php
index 42153b68d..ffe756c9d 100644
--- a/Helper/HelperSet.php
+++ b/Helper/HelperSet.php
@@ -35,7 +35,7 @@ public function __construct(array $helpers = [])
}
}
- public function set(HelperInterface $helper, string $alias = null): void
+ public function set(HelperInterface $helper, ?string $alias = null): void
{
$this->helpers[$helper->getName()] = $helper;
if (null !== $alias) {
@@ -61,7 +61,7 @@ public function has(string $name): bool
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];
diff --git a/Helper/OutputWrapper.php b/Helper/OutputWrapper.php
index 2ec819c74..a615ed2f9 100644
--- a/Helper/OutputWrapper.php
+++ b/Helper/OutputWrapper.php
@@ -49,7 +49,7 @@ final class OutputWrapper
private const URL_PATTERN = 'https?://\S+';
public function __construct(
- private bool $allowCutUrls = false
+ private bool $allowCutUrls = false,
) {
}
@@ -59,7 +59,7 @@ public function wrap(string $text, int $width, string $break = "\n"): string
return $text;
}
- $tagPattern = sprintf('<(?:(?:%s)|/(?:%s)?)>', self::TAG_OPEN_REGEX_SEGMENT, self::TAG_CLOSE_REGEX_SEGMENT);
+ $tagPattern = \sprintf('<(?:(?:%s)|/(?:%s)?)>', self::TAG_OPEN_REGEX_SEGMENT, self::TAG_CLOSE_REGEX_SEGMENT);
$limitPattern = "{1,$width}";
$patternBlocks = [$tagPattern];
if (!$this->allowCutUrls) {
@@ -68,7 +68,7 @@ public function wrap(string $text, int $width, string $break = "\n"): string
$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);
+ $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 26d35a1a8..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, array|Process $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".');
@@ -55,7 +55,7 @@ public function run(OutputInterface $output, array|Process $cmd, string $error =
$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()) {
@@ -69,12 +69,12 @@ public function run(OutputInterface $output, array|Process $cmd, string $error =
$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;
@@ -94,9 +94,9 @@ public function run(OutputInterface $output, array|Process $cmd, string $error =
*
* @see run()
*/
- public function mustRun(OutputInterface $output, array|Process $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);
@@ -108,7 +108,7 @@ public function mustRun(OutputInterface $output, array|Process $cmd, string $err
/**
* Wraps a Process callback to add debugging output.
*/
- public function wrapCallback(OutputInterface $output, Process $process, callable $callback = null): callable
+ public function wrapCallback(OutputInterface $output, Process $process, ?callable $callback = null): callable
{
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
diff --git a/Helper/ProgressBar.php b/Helper/ProgressBar.php
index 64389c4a2..dc3605ad2 100644
--- a/Helper/ProgressBar.php
+++ b/Helper/ProgressBar.php
@@ -183,9 +183,9 @@ public function setMessage(string $message, string $name = 'message'): void
$this->messages[$name] = $message;
}
- public function getMessage(string $name = 'message'): string
+ public function getMessage(string $name = 'message'): ?string
{
- return $this->messages[$name];
+ return $this->messages[$name] ?? null;
}
public function getStartTime(): int
@@ -195,7 +195,7 @@ public function getStartTime(): int
public function getMaxSteps(): int
{
- return $this->max;
+ return $this->max ?? 0;
}
public function getProgress(): int
@@ -215,7 +215,7 @@ 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
@@ -229,7 +229,7 @@ public function getEstimated(): float
public function getRemaining(): float
{
- if (!$this->step) {
+ if (0 === $this->step || $this->step === $this->startingStep) {
return 0;
}
@@ -253,7 +253,7 @@ public function setBarCharacter(string $char): void
public function getBarCharacter(): string
{
- return $this->barChar ?? ($this->max ? '=' : $this->emptyBarChar);
+ return $this->barChar ?? (null !== $this->max ? '=' : $this->emptyBarChar);
}
public function setEmptyBarCharacter(string $char): void
@@ -313,9 +313,23 @@ public function maxSecondsBetweenRedraws(float $seconds): void
*
* @return iterable
*/
- public function iterate(iterable $iterable, int $max = null): 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;
@@ -332,7 +346,7 @@ public function iterate(iterable $iterable, int $max = null): iterable
* @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, int $startAt = 0): void
+ public function start(?int $max = null, int $startAt = 0): void
{
$this->startTime = time();
$this->step = $startAt;
@@ -373,11 +387,15 @@ public function setProgress(int $step): void
$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
@@ -398,11 +416,20 @@ public function setProgress(int $step): void
}
}
- public function setMaxSteps(int $max): void
+ 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);
+ }
}
/**
@@ -410,16 +437,16 @@ public function setMaxSteps(int $max): void
*/
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);
}
/**
@@ -486,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");
@@ -542,14 +578,14 @@ private static function initPlaceholderFormatters(): array
},
'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(), 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.');
}
@@ -583,7 +619,7 @@ private function buildLine(): string
{
\assert(null !== $this->format);
- $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i";
+ $regex = '{%([a-z\-_]+)(?:\:([^%]+))?%}i';
$callback = function ($matches) {
if ($formatter = $this->getPlaceholderFormatter($matches[1])) {
$text = $formatter($this, $this->output);
@@ -594,7 +630,7 @@ private function buildLine(): string
}
if (isset($matches[2])) {
- $text = sprintf('%'.$matches[2], $text);
+ $text = \sprintf('%'.$matches[2], $text);
}
return $text;
diff --git a/Helper/ProgressIndicator.php b/Helper/ProgressIndicator.php
index 8865ecc34..b6bbd0cfa 100644
--- a/Helper/ProgressIndicator.php
+++ b/Helper/ProgressIndicator.php
@@ -31,15 +31,15 @@ class ProgressIndicator
'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)',
];
- private OutputInterface $output;
private int $startTime;
private ?string $format = null;
private ?string $message = null;
private array $indicatorValues;
private int $indicatorCurrent;
- private int $indicatorChangeInterval;
+ private string $finishedIndicatorValue;
private float $indicatorUpdateTime;
private bool $started = false;
+ private bool $finished = false;
/**
* @var array
@@ -50,21 +50,25 @@ class ProgressIndicator
* @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;
-
+ 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();
}
@@ -89,6 +93,7 @@ public function start(string $message): void
$this->message = $message;
$this->started = true;
+ $this->finished = false;
$this->startTime = time();
$this->indicatorUpdateTime = $this->getCurrentTimeInMilliseconds() + $this->indicatorChangeInterval;
$this->indicatorCurrent = 0;
@@ -123,13 +128,25 @@ public function advance(): void
/**
* Finish the indicator with message.
+ *
+ * @param ?string $finishedIndicator
*/
- public function finish(string $message): void
+ 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('');
@@ -172,7 +189,7 @@ private function display(): void
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);
}
@@ -216,7 +233,7 @@ private function getCurrentTimeInMilliseconds(): float
private static function initPlaceholderFormatters(): array
{
return [
- 'indicator' => fn (self $indicator) => $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)],
+ '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 cb75ac914..09b65bbf9 100644
--- a/Helper/QuestionHelper.php
+++ b/Helper/QuestionHelper.php
@@ -34,11 +34,6 @@
*/
class QuestionHelper extends Helper
{
- /**
- * @var resource|null
- */
- private $inputStream;
-
private static bool $stty = true;
private static bool $stdinIsInteractive;
@@ -59,16 +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 = fn () => $this->doAsk($output, $question);
+ $interviewer = fn () => $this->doAsk($inputStream, $output, $question);
return $this->validateAttempts($interviewer, $output, $question);
} catch (MissingInputException $exception) {
@@ -98,13 +92,14 @@ public static function disableStty(): void
/**
* Asks the question to the user.
*
+ * @param resource $inputStream
+ *
* @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
*/
- private function doAsk(OutputInterface $output, Question $question): mixed
+ 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()) {
@@ -216,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;
@@ -495,19 +490,7 @@ private function isInteractiveInput($inputStream): bool
return self::$stdinIsInteractive;
}
- if (\function_exists('stream_isatty')) {
- return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r'));
- }
-
- if (\function_exists('posix_isatty')) {
- return self::$stdinIsInteractive = @posix_isatty(fopen('php://stdin', 'r'));
- }
-
- if (!\function_exists('shell_exec')) {
- return self::$stdinIsInteractive = true;
- }
-
- return self::$stdinIsInteractive = (bool) shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null'));
+ return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r'));
}
/**
@@ -533,12 +516,16 @@ private function readInput($inputStream, Question $question): string|false
$ret = '';
$cp = $this->setIOCodepage();
while (false !== ($char = fgetc($multiLineStreamReader))) {
- if (\PHP_EOL === "{$ret}{$char}") {
+ if ("\x4" === $char || \PHP_EOL === "{$ret}{$char}") {
break;
}
$ret .= $char;
}
+ if (stream_get_meta_data($inputStream)['seekable']) {
+ fseek($inputStream, ftell($multiLineStreamReader));
+ }
+
return $this->resetIOCodepage($cp, $ret);
}
diff --git a/Helper/SymfonyQuestionHelper.php b/Helper/SymfonyQuestionHelper.php
index 48d947b75..b452bf047 100644
--- a/Helper/SymfonyQuestionHelper.php
+++ b/Helper/SymfonyQuestionHelper.php
@@ -31,17 +31,17 @@ protected function writePrompt(OutputInterface $output, Question $question): voi
$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;
@@ -53,18 +53,18 @@ protected function writePrompt(OutputInterface $output, Question $question): voi
$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);
diff --git a/Helper/Table.php b/Helper/Table.php
index fe2ac87c1..8c3d0a521 100644
--- a/Helper/Table.php
+++ b/Helper/Table.php
@@ -45,7 +45,6 @@ class Table
private array $rows = [];
private array $effectiveColumnWidths = [];
private int $numberOfColumns;
- private OutputInterface $output;
private TableStyle $style;
private array $columnStyles = [];
private array $columnWidths = [];
@@ -55,10 +54,9 @@ class Table
private static array $styles;
- public function __construct(OutputInterface $output)
- {
- $this->output = $output;
-
+ public function __construct(
+ private OutputInterface $output,
+ ) {
self::$styles ??= self::initStyles();
$this->setStyle('default');
@@ -81,7 +79,7 @@ public static function getStyleDefinition(string $name): TableStyle
{
self::$styles ??= self::initStyles();
- 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));
}
/**
@@ -166,7 +164,7 @@ public function setColumnWidths(array $widths): static
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;
@@ -235,7 +233,7 @@ public function addRow(TableSeparator|array $row): static
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) {
@@ -361,17 +359,19 @@ public function render(): void
for ($i = 0; $i < $maxRows; ++$i) {
$cell = (string) ($row[$i] ?? '');
- $parts = explode("\n", $cell);
+ $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',
- str_pad($headers[$i] ?? '', $maxHeaderLength, ' ', \STR_PAD_LEFT),
+ $rows[] = [\sprintf(
+ '%s%s>: %s',
+ str_repeat(' ', $maxHeaderLength - Helper::width(Helper::removeDecoration($formatter, $headers[$i] ?? ''))),
+ $headers[$i] ?? '',
$part
)];
} else {
- $rows[] = [sprintf(
+ $rows[] = [\sprintf(
'%s %s',
str_pad('', $maxHeaderLength, ' ', \STR_PAD_LEFT),
$part
@@ -417,7 +417,7 @@ public function render(): void
continue;
}
- if ($isHeader && !$isHeaderSeparatorRendered) {
+ if ($isHeader && !$isHeaderSeparatorRendered && $this->style->displayOutsideBorder()) {
$this->renderRowSeparator(
self::SEPARATOR_TOP,
$hasTitle ? $this->headerTitle : null,
@@ -449,7 +449,10 @@ public function render(): void
}
}
}
- $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat());
+
+ if ($this->getStyle()->displayOutsideBorder()) {
+ $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat());
+ }
$this->cleanup();
$this->rendered = true;
@@ -462,7 +465,7 @@ public function render(): void
*
* +-----+-----------+-------+
*/
- private function renderRowSeparator(int $type = self::SEPARATOR_MID, string $title = null, string $titleFormat = null): void
+ private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null): void
{
if (!$count = $this->numberOfColumns) {
return;
@@ -491,12 +494,12 @@ private function renderRowSeparator(int $type = self::SEPARATOR_MID, string $tit
}
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);
@@ -507,7 +510,7 @@ private function renderRowSeparator(int $type = self::SEPARATOR_MID, string $tit
}
}
- $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup));
+ $this->output->writeln(\sprintf($this->style->getBorderFormat(), $markup));
}
/**
@@ -517,7 +520,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]);
}
/**
@@ -527,7 +530,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): void
+ private function renderRow(array $row, string $cellFormat, ?string $firstCellFormat = null): void
{
$rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE);
$columns = $this->getRowColumns($row);
@@ -558,18 +561,15 @@ private function renderCell(array $row, int $column, string $cellFormat): string
}
// str_pad won't work properly with multi-byte strings, we need to fix the padding
- if (false !== $encoding = mb_detect_encoding($cell, null, true)) {
- $width += \strlen($cell) - mb_strwidth($cell, $encoding);
- }
-
+ $width += \strlen($cell) - Helper::width($cell) - substr_count($cell, "\0");
$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) {
@@ -594,7 +594,7 @@ 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));
}
/**
@@ -626,15 +626,56 @@ private function buildTableRows(array $rows): TableRows
foreach ($rows[$rowKey] as $column => $cell) {
$colspan = $cell instanceof TableCell ? $cell->getColspan() : 1;
- if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) {
- $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
+ $minWrappedWidth = 0;
+ $widthApplied = [];
+ $lengthColumnBorder = $this->getColumnSeparatorWidth() + Helper::width($this->style->getCellRowContentFormat()) - 2;
+ for ($i = $column; $i < ($column + $colspan); ++$i) {
+ if (isset($this->columnMaxWidths[$i])) {
+ $minWrappedWidth += $this->columnMaxWidths[$i];
+ $widthApplied[] = ['type' => 'max', 'column' => $i];
+ } elseif (($this->columnWidths[$i] ?? 0) > 0 && $colspan > 1) {
+ $minWrappedWidth += $this->columnWidths[$i];
+ $widthApplied[] = ['type' => 'min', 'column' => $i];
+ }
+ }
+ if (1 === \count($widthApplied)) {
+ if ($colspan > 1) {
+ $minWrappedWidth *= $colspan; // previous logic
+ }
+ } elseif (\count($widthApplied) > 1) {
+ $minWrappedWidth += (\count($widthApplied) - 1) * $lengthColumnBorder;
+ }
+
+ $cellWidth = Helper::width(Helper::removeDecoration($formatter, $cell));
+ if ($minWrappedWidth && $cellWidth > $minWrappedWidth) {
+ $cell = $formatter->formatAndWrap($cell, $minWrappedWidth);
+ }
+ // update minimal columnWidths for spanned columns
+ if ($colspan > 1 && $minWrappedWidth > 0) {
+ $columnsMinWidthProcessed = [];
+ $cellWidth = min($cellWidth, $minWrappedWidth);
+ foreach ($widthApplied as $item) {
+ if ('max' === $item['type'] && $cellWidth >= $this->columnMaxWidths[$item['column']]) {
+ $minWidthColumn = $this->columnMaxWidths[$item['column']];
+ $this->columnWidths[$item['column']] = $minWidthColumn;
+ $columnsMinWidthProcessed[$item['column']] = true;
+ $cellWidth -= $minWidthColumn + $lengthColumnBorder;
+ }
+ }
+ for ($i = $column; $i < ($column + $colspan); ++$i) {
+ if (isset($columnsMinWidthProcessed[$i])) {
+ continue;
+ }
+ $this->columnWidths[$i] = $cellWidth + $lengthColumnBorder;
+ }
}
if (!str_contains($cell ?? '', "\n")) {
continue;
}
- $escaped = implode("\n", array_map(OutputFormatter::escapeTrailingBackslash(...), explode("\n", $cell)));
+ $eol = str_contains($cell ?? '', "\r\n") ? "\r\n" : "\n";
+ $escaped = implode($eol, array_map(OutputFormatter::escapeTrailingBackslash(...), explode($eol, $cell)));
$cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped;
- $lines = explode("\n", str_replace("\n", ">\n", $cell));
+ $lines = explode($eol, str_replace($eol, '>'.$eol, $cell));
foreach ($lines as $lineKey => $line) {
if ($colspan > 1) {
$line = new TableCell($line, ['colspan' => $colspan]);
@@ -690,14 +731,15 @@ private function fillNextRows(array $rows, int $line): array
$unmergedRows = [];
foreach ($rows[$line] as $column => $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)));
+ 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 (str_contains($cell, "\n")) {
- $lines = explode("\n", str_replace("\n", "\n>", $cell));
- $nbLines = \count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines;
+ $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;
$rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
unset($lines[0]);
@@ -717,7 +759,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]);
@@ -725,8 +767,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]);
@@ -834,7 +876,7 @@ private function calculateColumnsWidth(iterable $groups): void
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
@@ -866,6 +908,12 @@ private function cleanup(): void
*/
private static function initStyles(): array
{
+ $markdown = new TableStyle();
+ $markdown
+ ->setDefaultCrossingChar('|')
+ ->setDisplayOutsideBorder(false)
+ ;
+
$borderless = new TableStyle();
$borderless
->setHorizontalBorderChars('=')
@@ -903,6 +951,7 @@ private static function initStyles(): array
return [
'default' => new TableStyle(),
+ 'markdown' => $markdown,
'borderless' => $borderless,
'compact' => $compact,
'symfony-style-guide' => $styleGuide,
@@ -917,6 +966,6 @@ private function resolveStyle(TableStyle|string $name): TableStyle
return $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 394b2bc95..ab8339204 100644
--- a/Helper/TableCell.php
+++ b/Helper/TableCell.php
@@ -18,20 +18,19 @@
*/
class TableCell
{
- private string $value;
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) {
diff --git a/Helper/TableCellStyle.php b/Helper/TableCellStyle.php
index 9419dcb40..af1a17e96 100644
--- a/Helper/TableCellStyle.php
+++ b/Helper/TableCellStyle.php
@@ -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);
@@ -67,7 +67,7 @@ public function getTagOptions(): array
{
return array_filter(
$this->getOptions(),
- fn ($key) => \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
);
}
diff --git a/Helper/TableRows.php b/Helper/TableRows.php
index 97d07726e..fb2dc2789 100644
--- a/Helper/TableRows.php
+++ b/Helper/TableRows.php
@@ -16,11 +16,9 @@
*/
class TableRows implements \IteratorAggregate
{
- private \Closure $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 bbad98e73..74ac58925 100644
--- a/Helper/TableStyle.php
+++ b/Helper/TableStyle.php
@@ -46,6 +46,7 @@ class TableStyle
private string $cellRowFormat = '%s';
private string $cellRowContentFormat = ' %s ';
private string $borderFormat = '%s';
+ private bool $displayOutsideBorder = true;
private int $padType = \STR_PAD_RIGHT;
/**
@@ -88,7 +89,7 @@ public function getPaddingChar(): string
*
* @return $this
*/
- public function setHorizontalBorderChars(string $outside, string $inside = null): static
+ public function setHorizontalBorderChars(string $outside, ?string $inside = null): static
{
$this->horizontalOutsideBorderChar = $outside;
$this->horizontalInsideBorderChar = $inside ?? $outside;
@@ -113,7 +114,7 @@ public function setHorizontalBorderChars(string $outside, string $inside = null)
*
* @return $this
*/
- public function setVerticalBorderChars(string $outside, string $inside = null): static
+ public function setVerticalBorderChars(string $outside, ?string $inside = null): static
{
$this->verticalOutsideBorderChar = $outside;
$this->verticalInsideBorderChar = $inside ?? $outside;
@@ -167,7 +168,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): static
+ 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;
@@ -359,4 +360,16 @@ public function setFooterTitleFormat(string $format): static
return $this;
}
+
+ public function setDisplayOutsideBorder($displayOutSideBorder): static
+ {
+ $this->displayOutsideBorder = $displayOutSideBorder;
+
+ return $this;
+ }
+
+ public function displayOutsideBorder(): bool
+ {
+ return $this->displayOutsideBorder;
+ }
}
diff --git a/Helper/TreeHelper.php b/Helper/TreeHelper.php
new file mode 100644
index 000000000..d188afe98
--- /dev/null
+++ b/Helper/TreeHelper.php
@@ -0,0 +1,111 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Helper;
+
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * The TreeHelper class provides methods to display tree-like structures.
+ *
+ * @author Simon André
+ *
+ * @implements \RecursiveIterator
+ */
+final class TreeHelper implements \RecursiveIterator
+{
+ /**
+ * @var \Iterator
+ */
+ private \Iterator $children;
+
+ private function __construct(
+ private readonly OutputInterface $output,
+ private readonly TreeNode $node,
+ private readonly TreeStyle $style,
+ ) {
+ $this->children = new \IteratorIterator($this->node->getChildren());
+ $this->children->rewind();
+ }
+
+ public static function createTree(OutputInterface $output, string|TreeNode|null $root = null, iterable $values = [], ?TreeStyle $style = null): self
+ {
+ $node = $root instanceof TreeNode ? $root : new TreeNode($root ?? '');
+
+ return new self($output, TreeNode::fromValues($values, $node), $style ?? TreeStyle::default());
+ }
+
+ public function current(): TreeNode
+ {
+ return $this->children->current();
+ }
+
+ public function key(): int
+ {
+ return $this->children->key();
+ }
+
+ public function next(): void
+ {
+ $this->children->next();
+ }
+
+ public function rewind(): void
+ {
+ $this->children->rewind();
+ }
+
+ public function valid(): bool
+ {
+ return $this->children->valid();
+ }
+
+ public function hasChildren(): bool
+ {
+ if (null === $current = $this->current()) {
+ return false;
+ }
+
+ foreach ($current->getChildren() as $child) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public function getChildren(): \RecursiveIterator
+ {
+ return new self($this->output, $this->current(), $this->style);
+ }
+
+ /**
+ * Recursively renders the tree to the output, applying the tree style.
+ */
+ public function render(): void
+ {
+ $treeIterator = new \RecursiveTreeIterator($this);
+
+ $this->style->applyPrefixes($treeIterator);
+
+ $this->output->writeln($this->node->getValue());
+
+ $visited = new \SplObjectStorage();
+ foreach ($treeIterator as $node) {
+ $currentNode = $node instanceof TreeNode ? $node : $treeIterator->getInnerIterator()->current();
+ if (isset($visited[$currentNode])) {
+ throw new \LogicException(\sprintf('Cycle detected at node: "%s".', $currentNode->getValue()));
+ }
+ $visited[$currentNode] = true;
+
+ $this->output->writeln($node);
+ }
+ }
+}
diff --git a/Helper/TreeNode.php b/Helper/TreeNode.php
new file mode 100644
index 000000000..8c35266c1
--- /dev/null
+++ b/Helper/TreeNode.php
@@ -0,0 +1,105 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Helper;
+
+/**
+ * @implements \IteratorAggregate
+ *
+ * @author Simon André
+ */
+final class TreeNode implements \Countable, \IteratorAggregate
+{
+ /**
+ * @var array
+ */
+ private array $children = [];
+
+ public function __construct(
+ private readonly string $value = '',
+ iterable $children = [],
+ ) {
+ foreach ($children as $child) {
+ $this->addChild($child);
+ }
+ }
+
+ public static function fromValues(iterable $nodes, ?self $node = null): self
+ {
+ $node ??= new self();
+ foreach ($nodes as $key => $value) {
+ if (is_iterable($value)) {
+ $child = new self($key);
+ self::fromValues($value, $child);
+ $node->addChild($child);
+ } elseif ($value instanceof self) {
+ $node->addChild($value);
+ } else {
+ $node->addChild(new self($value));
+ }
+ }
+
+ return $node;
+ }
+
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+
+ public function addChild(self|string|callable $node): self
+ {
+ if (\is_string($node)) {
+ $node = new self($node);
+ }
+
+ $this->children[] = $node;
+
+ return $this;
+ }
+
+ /**
+ * @return \Traversable
+ */
+ public function getChildren(): \Traversable
+ {
+ foreach ($this->children as $child) {
+ if (\is_callable($child)) {
+ yield from $child();
+ } elseif ($child instanceof self) {
+ yield $child;
+ }
+ }
+ }
+
+ /**
+ * @return \Traversable
+ */
+ public function getIterator(): \Traversable
+ {
+ return $this->getChildren();
+ }
+
+ public function count(): int
+ {
+ $count = 0;
+ foreach ($this->getChildren() as $child) {
+ ++$count;
+ }
+
+ return $count;
+ }
+
+ public function __toString(): string
+ {
+ return $this->value;
+ }
+}
diff --git a/Helper/TreeStyle.php b/Helper/TreeStyle.php
new file mode 100644
index 000000000..21cc04b3c
--- /dev/null
+++ b/Helper/TreeStyle.php
@@ -0,0 +1,78 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Helper;
+
+/**
+ * Configures the output of the Tree helper.
+ *
+ * @author Simon André
+ */
+final class TreeStyle
+{
+ public function __construct(
+ private readonly string $prefixEndHasNext,
+ private readonly string $prefixEndLast,
+ private readonly string $prefixLeft,
+ private readonly string $prefixMidHasNext,
+ private readonly string $prefixMidLast,
+ private readonly string $prefixRight,
+ ) {
+ }
+
+ public static function box(): self
+ {
+ return new self('┃╸ ', '┗╸ ', '', '┃ ', ' ', '');
+ }
+
+ public static function boxDouble(): self
+ {
+ return new self('╠═ ', '╚═ ', '', '║ ', ' ', '');
+ }
+
+ public static function compact(): self
+ {
+ return new self('├ ', '└ ', '', '│ ', ' ', '');
+ }
+
+ public static function default(): self
+ {
+ return new self('├── ', '└── ', '', '│ ', ' ', '');
+ }
+
+ public static function light(): self
+ {
+ return new self('|-- ', '`-- ', '', '| ', ' ', '');
+ }
+
+ public static function minimal(): self
+ {
+ return new self('. ', '. ', '', '. ', ' ', '');
+ }
+
+ public static function rounded(): self
+ {
+ return new self('├─ ', '╰─ ', '', '│ ', ' ', '');
+ }
+
+ /**
+ * @internal
+ */
+ public function applyPrefixes(\RecursiveTreeIterator $iterator): void
+ {
+ $iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_LEFT, $this->prefixLeft);
+ $iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_MID_HAS_NEXT, $this->prefixMidHasNext);
+ $iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_MID_LAST, $this->prefixMidLast);
+ $iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_END_HAS_NEXT, $this->prefixEndHasNext);
+ $iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_END_LAST, $this->prefixEndLast);
+ $iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_RIGHT, $this->prefixRight);
+ }
+}
diff --git a/Input/ArgvInput.php b/Input/ArgvInput.php
index 9ae2f54f5..d7c57f688 100644
--- a/Input/ArgvInput.php
+++ b/Input/ArgvInput.php
@@ -40,13 +40,21 @@
*/
class ArgvInput extends Input
{
+ /** @var list */
private array $tokens;
private array $parsed;
- public function __construct(array $argv = null, InputDefinition $definition = null)
+ /** @param list|null $argv */
+ public function __construct(?array $argv = null, ?InputDefinition $definition = null)
{
$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,6 +63,7 @@ public function __construct(array $argv = null, InputDefinition $definition = nu
parent::__construct($definition);
}
+ /** @param list $tokens */
protected function setTokens(array $tokens): void
{
$this->tokens = $tokens;
@@ -116,7 +125,7 @@ private function parseShortOptionSet(string $name): void
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]);
@@ -124,9 +133,9 @@ private function parseShortOptionSet(string $name): void
$this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1));
break;
- } else {
- $this->addLongOption($option->getName(), null);
}
+
+ $this->addLongOption($option->getName(), null);
}
}
@@ -170,21 +179,21 @@ private function parseArgument(string $token): void
} else {
$all = $this->definition->getArguments();
$symfonyCommandName = null;
- if (($inputArgument = $all[$key = array_key_first($all)] ?? null) && 'command' === $inputArgument->getName()) {
+ if (($inputArgument = $all[$key = array_key_first($all) ?? ''] ?? null) && 'command' === $inputArgument->getName()) {
$symfonyCommandName = $this->arguments['command'] ?? null;
unset($all[$key]);
}
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,7 +208,7 @@ private function parseArgument(string $token): void
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);
@@ -214,12 +223,12 @@ 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;
@@ -229,7 +238,7 @@ private function addLongOption(string $name, mixed $value): void
$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)) {
@@ -245,7 +254,7 @@ private function addLongOption(string $name, mixed $value): void
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()) {
@@ -342,6 +351,35 @@ public function getParameterOption(string|array $values, string|bool|int|float|a
return $default;
}
+ /**
+ * 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 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.
*/
diff --git a/Input/ArrayInput.php b/Input/ArrayInput.php
index 03b200b13..7335632bf 100644
--- a/Input/ArrayInput.php
+++ b/Input/ArrayInput.php
@@ -25,12 +25,10 @@
*/
class ArrayInput extends Input
{
- private array $parameters;
-
- public function __construct(array $parameters, InputDefinition $definition = null)
- {
- $this->parameters = $parameters;
-
+ public function __construct(
+ private array $parameters,
+ ?InputDefinition $definition = null,
+ ) {
parent::__construct($definition);
}
@@ -137,7 +135,7 @@ protected function parse(): void
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);
@@ -153,7 +151,7 @@ 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);
@@ -166,7 +164,7 @@ private function addLongOption(string $name, mixed $value): void
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()) {
@@ -185,7 +183,7 @@ private function addLongOption(string $name, mixed $value): void
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 6a9248b7a..d2881c60f 100644
--- a/Input/Input.php
+++ b/Input/Input.php
@@ -34,7 +34,7 @@ abstract class Input implements InputInterface, StreamableInputInterface
protected array $arguments = [];
protected bool $interactive = true;
- public function __construct(InputDefinition $definition = null)
+ public function __construct(?InputDefinition $definition = null)
{
if (null === $definition) {
$this->definition = new InputDefinition();
@@ -66,7 +66,7 @@ public function validate(): void
$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)));
}
}
@@ -88,7 +88,7 @@ public function getArguments(): array
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();
@@ -97,7 +97,7 @@ public function getArgument(string $name): mixed
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;
@@ -124,7 +124,7 @@ public function getOption(string $name): mixed
}
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();
@@ -137,7 +137,7 @@ public function setOption(string $name, mixed $value): void
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;
diff --git a/Input/InputArgument.php b/Input/InputArgument.php
index 642ae6600..6fbb64ed0 100644
--- a/Input/InputArgument.php
+++ b/Input/InputArgument.php
@@ -25,37 +25,47 @@
*/
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 string $name;
private int $mode;
- private string|int|bool|array|null|float $default;
- private array|\Closure $suggestedValues;
- private string $description;
+ 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 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 = '', string|bool|int|float|array $default = null, \Closure|array $suggestedValues = [])
- {
+ 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->suggestedValues = $suggestedValues;
$this->setDefault($default);
}
@@ -90,8 +100,6 @@ public function isArray(): bool
/**
* Sets the default value.
- *
- * @throws LogicException When incorrect default value is given
*/
public function setDefault(string|bool|int|float|array|null $default): void
{
@@ -118,13 +126,16 @@ public function getDefault(): string|bool|int|float|array|null
return $this->default;
}
+ /**
+ * Returns true if the argument has values for input completion.
+ */
public function hasCompletion(): bool
{
return [] !== $this->suggestedValues;
}
/**
- * Adds suggestions to $suggestions for the current completion input.
+ * Supplies suggestions when command resolves possible completion options for input.
*
* @see Command::complete()
*/
@@ -132,7 +143,7 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti
{
$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)));
+ 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);
diff --git a/Input/InputDefinition.php b/Input/InputDefinition.php
index f27e29748..a8b006d48 100644
--- a/Input/InputDefinition.php
+++ b/Input/InputDefinition.php
@@ -97,15 +97,15 @@ public function addArguments(?array $arguments = []): void
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()) {
@@ -129,7 +129,7 @@ public function addArgument(InputArgument $argument): void
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;
@@ -217,16 +217,16 @@ public function addOptions(array $options = []): void
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));
}
}
}
@@ -241,7 +241,7 @@ public function addOption(InputOption $option): void
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();
}
@@ -255,7 +255,7 @@ public function addOption(InputOption $option): void
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];
@@ -329,7 +329,7 @@ public function getOptionDefaults(): array
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];
@@ -345,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];
@@ -364,7 +364,7 @@ public function getSynopsis(bool $short = false): string
foreach ($this->getOptions() as $option) {
$value = '';
if ($option->acceptValue()) {
- $value = sprintf(
+ $value = \sprintf(
' %s%s%s',
$option->isValueOptional() ? '[' : '',
strtoupper($option->getName()),
@@ -372,9 +372,9 @@ public function getSynopsis(bool $short = false): string
);
}
- $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/InputOption.php b/Input/InputOption.php
index f8e9b0dd6..25fb91782 100644
--- a/Input/InputOption.php
+++ b/Input/InputOption.php
@@ -46,36 +46,40 @@ 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 string $name;
- private string|array|null $shortcut;
+ private ?string $shortcut;
private int $mode;
- private string|int|bool|array|null|float $default;
- private array|\Closure $suggestedValues;
- private string $description;
+ 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 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, string|array $shortcut = null, int $mode = null, string $description = '', string|bool|int|float|array $default = null, array|\Closure $suggestedValues = [])
- {
+ 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.');
}
- if (empty($shortcut)) {
+ if ('' === $shortcut || [] === $shortcut || false === $shortcut) {
$shortcut = null;
}
@@ -84,10 +88,10 @@ public function __construct(string $name, string|array $shortcut = null, int $mo
$shortcut = implode('|', $shortcut);
}
$shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-'));
- $shortcuts = array_filter($shortcuts);
+ $shortcuts = array_filter($shortcuts, 'strlen');
$shortcut = implode('|', $shortcuts);
- if (empty($shortcut)) {
+ if ('' === $shortcut) {
throw new InvalidArgumentException('An option shortcut cannot be empty.');
}
}
@@ -95,14 +99,12 @@ public function __construct(string $name, string|array $shortcut = null, int $mo
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;
- $this->suggestedValues = $suggestedValues;
if ($suggestedValues && !$this->acceptValue()) {
throw new LogicException('Cannot set suggested values if the option does not accept a value.');
@@ -173,11 +175,19 @@ 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);
}
+ /**
+ * Sets the default value.
+ */
public function setDefault(string|bool|int|float|array|null $default): void
{
if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) {
@@ -211,13 +221,16 @@ public function getDescription(): string
return $this->description;
}
+ /**
+ * Returns true if the option has values for input completion.
+ */
public function hasCompletion(): bool
{
return [] !== $this->suggestedValues;
}
/**
- * Adds suggestions to $suggestions for the current completion input.
+ * Supplies suggestions when command resolves possible completion options for input.
*
* @see Command::complete()
*/
@@ -225,7 +238,7 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti
{
$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)));
+ 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);
diff --git a/Input/StringInput.php b/Input/StringInput.php
index 33f0f4b39..a70f048f9 100644
--- a/Input/StringInput.php
+++ b/Input/StringInput.php
@@ -40,6 +40,8 @@ public function __construct(string $input)
/**
* Tokenizes a string.
*
+ * @return list
+ *
* @throws InvalidArgumentException When unable to parse input (should never happen)
*/
private function tokenize(string $input): array
@@ -68,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 fddef50cd..a6ef49ea9 100644
--- a/Logger/ConsoleLogger.php
+++ b/Logger/ConsoleLogger.php
@@ -29,7 +29,6 @@ class ConsoleLogger extends AbstractLogger
public const INFO = 'info';
public const ERROR = 'error';
- private OutputInterface $output;
private array $verbosityLevelMap = [
LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL,
LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL,
@@ -52,9 +51,11 @@ class ConsoleLogger extends AbstractLogger
];
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;
}
@@ -62,7 +63,7 @@ public function __construct(OutputInterface $output, array $verbosityLevelMap =
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;
@@ -78,7 +79,7 @@ public function log($level, $message, array $context = []): void
// 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]);
}
}
diff --git a/Messenger/RunCommandMessageHandler.php b/Messenger/RunCommandMessageHandler.php
index 14f9c1764..df5f48af0 100644
--- a/Messenger/RunCommandMessageHandler.php
+++ b/Messenger/RunCommandMessageHandler.php
@@ -16,14 +16,17 @@
use Symfony\Component\Console\Exception\RunCommandFailedException;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\BufferedOutput;
+use Symfony\Component\Messenger\Exception\RecoverableExceptionInterface;
+use Symfony\Component\Messenger\Exception\UnrecoverableExceptionInterface;
/**
* @author Kevin Bond
*/
final class RunCommandMessageHandler
{
- public function __construct(private readonly Application $application)
- {
+ public function __construct(
+ private readonly Application $application,
+ ) {
}
public function __invoke(RunCommandMessage $message): RunCommandContext
@@ -35,12 +38,14 @@ public function __invoke(RunCommandMessage $message): RunCommandContext
try {
$exitCode = $this->application->run($input, $output);
+ } catch (UnrecoverableExceptionInterface|RecoverableExceptionInterface $e) {
+ throw $e;
} 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()));
+ 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
index 5f9f744fe..0e1422a27 100644
--- a/Output/AnsiColorMode.php
+++ b/Output/AnsiColorMode.php
@@ -51,7 +51,7 @@ public function convertFromHexToAnsiColorCode(string $hexColor): string
}
if (6 !== \strlen($hexColor)) {
- throw new InvalidArgumentException(sprintf('Invalid "#%s" color.', $hexColor));
+ throw new InvalidArgumentException(\sprintf('Invalid "#%s" color.', $hexColor));
}
$color = hexdec($hexColor);
@@ -62,8 +62,8 @@ public function convertFromHexToAnsiColorCode(string $hexColor): string
return match ($this) {
self::Ansi4 => (string) $this->convertFromRGB($r, $g, $b),
- self::Ansi8 => '8;5;'.((string) $this->convertFromRGB($r, $g, $b)),
- self::Ansi24 => sprintf('8;2;%d;%d;%d', $r, $g, $b)
+ self::Ansi8 => '8;5;'.$this->convertFromRGB($r, $g, $b),
+ self::Ansi24 => \sprintf('8;2;%d;%d;%d', $r, $g, $b),
};
}
@@ -72,7 +72,7 @@ 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}.")
+ default => throw new InvalidArgumentException("RGB cannot be converted to {$this->name}."),
};
}
@@ -96,11 +96,11 @@ private function degradeHexColorToAnsi8(int $r, int $g, int $b): int
}
return (int) round(($r - 8) / 247 * 24) + 232;
- } else {
- return 16 +
- (36 * (int) round($r / 255 * 5)) +
- (6 * (int) round($g / 255 * 5)) +
- (int) round($b / 255 * 5);
}
+
+ return 16 +
+ (36 * (int) round($r / 255 * 5)) +
+ (6 * (int) round($g / 255 * 5)) +
+ (int) round($b / 255 * 5);
}
}
diff --git a/Output/ConsoleOutput.php b/Output/ConsoleOutput.php
index f9e6c7710..2ad3dbcf3 100644
--- a/Output/ConsoleOutput.php
+++ b/Output/ConsoleOutput.php
@@ -37,7 +37,7 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
* @param bool|null $decorated Whether to decorate messages (null for auto-guessing)
* @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter)
*/
- public function __construct(int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = null, OutputFormatterInterface $formatter = null)
+ public function __construct(int $verbosity = self::VERBOSITY_NORMAL, ?bool $decorated = null, ?OutputFormatterInterface $formatter = null)
{
parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter);
diff --git a/Output/ConsoleSectionOutput.php b/Output/ConsoleSectionOutput.php
index d5b5aff76..44728dfd4 100644
--- a/Output/ConsoleSectionOutput.php
+++ b/Output/ConsoleSectionOutput.php
@@ -61,9 +61,9 @@ public function setMaxHeight(int $maxHeight): void
*
* @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): void
+ public function clear(?int $lines = null): void
{
- if (empty($this->content) || !$this->isDecorated()) {
+ if (!$this->content || !$this->isDecorated()) {
return;
}
@@ -222,7 +222,7 @@ private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFr
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 40ae33282..8bec706d4 100644
--- a/Output/NullOutput.php
+++ b/Output/NullOutput.php
@@ -54,14 +54,19 @@ public function setVerbosity(int $level): void
public function getVerbosity(): int
{
- return self::VERBOSITY_QUIET;
+ return self::VERBOSITY_SILENT;
}
- public function isQuiet(): bool
+ public function isSilent(): bool
{
return true;
}
+ public function isQuiet(): bool
+ {
+ return false;
+ }
+
public function isVerbose(): bool
{
return false;
diff --git a/Output/Output.php b/Output/Output.php
index fe8564bb9..32e6cb241 100644
--- a/Output/Output.php
+++ b/Output/Output.php
@@ -17,13 +17,14 @@
/**
* 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
*/
@@ -37,7 +38,7 @@ abstract class Output implements OutputInterface
* @param bool $decorated Whether to decorate messages
* @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter)
*/
- public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, OutputFormatterInterface $formatter = null)
+ public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, ?OutputFormatterInterface $formatter = null)
{
$this->verbosity = $verbosity ?? self::VERBOSITY_NORMAL;
$this->formatter = $formatter ?? new OutputFormatter();
@@ -74,6 +75,11 @@ public function getVerbosity(): int
return $this->verbosity;
}
+ public function isSilent(): bool
+ {
+ return self::VERBOSITY_SILENT === $this->verbosity;
+ }
+
public function isQuiet(): bool
{
return self::VERBOSITY_QUIET === $this->verbosity;
diff --git a/Output/OutputInterface.php b/Output/OutputInterface.php
index 41315fbf2..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;
diff --git a/Output/StreamOutput.php b/Output/StreamOutput.php
index f5119ea0b..ce5a825e8 100644
--- a/Output/StreamOutput.php
+++ b/Output/StreamOutput.php
@@ -40,7 +40,7 @@ class StreamOutput extends Output
*
* @throws InvalidArgumentException When first argument is not a real stream
*/
- public function __construct($stream, int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = null, OutputFormatterInterface $formatter = null)
+ public function __construct($stream, int $verbosity = self::VERBOSITY_NORMAL, ?bool $decorated = null, ?OutputFormatterInterface $formatter = null)
{
if (!\is_resource($stream) || 'stream' !== get_resource_type($stream)) {
throw new InvalidArgumentException('The StreamOutput class needs a stream as its first argument.');
@@ -90,22 +90,38 @@ protected function doWrite(string $message, bool $newline): void
protected function hasColorSupport(): bool
{
// Follow https://no-color.org/
- if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) {
+ if ('' !== (($_SERVER['NO_COLOR'] ?? getenv('NO_COLOR'))[0] ?? '')) {
return false;
}
- if ('Hyper' === getenv('TERM_PROGRAM')) {
+ // Follow https://force-color.org/
+ if ('' !== (($_SERVER['FORCE_COLOR'] ?? getenv('FORCE_COLOR'))[0] ?? '')) {
return true;
}
- if (\DIRECTORY_SEPARATOR === '\\') {
- return (\function_exists('sapi_windows_vt100_support')
- && @sapi_windows_vt100_support($this->stream))
- || false !== getenv('ANSICON')
- || 'ON' === getenv('ConEmuANSI')
- || 'xterm' === getenv('TERM');
+ // 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)) {
+ return false;
+ }
+
+ if ('\\' === \DIRECTORY_SEPARATOR && @sapi_windows_vt100_support($this->stream)) {
+ return true;
+ }
+
+ if ('Hyper' === getenv('TERM_PROGRAM')
+ || false !== getenv('COLORTERM')
+ || false !== getenv('ANSICON')
+ || 'ON' === getenv('ConEmuANSI')
+ ) {
+ return true;
+ }
+
+ if ('dumb' === $term = (string) getenv('TERM')) {
+ return false;
}
- return stream_isatty($this->stream);
+ // See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157
+ return preg_match('/^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/', $term);
}
}
diff --git a/Output/TrimmedBufferOutput.php b/Output/TrimmedBufferOutput.php
index 5655e7bc8..33db072c5 100644
--- a/Output/TrimmedBufferOutput.php
+++ b/Output/TrimmedBufferOutput.php
@@ -24,10 +24,10 @@ class TrimmedBufferOutput extends Output
private int $maxLength;
private string $buffer = '';
- public function __construct(int $maxLength, ?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, OutputFormatterInterface $formatter = null)
+ 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);
@@ -53,6 +53,6 @@ protected function doWrite(string $message, bool $newline): void
$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 e449ff683..36c240d37 100644
--- a/Question/ChoiceQuestion.php
+++ b/Question/ChoiceQuestion.php
@@ -20,25 +20,26 @@
*/
class ChoiceQuestion extends Question
{
- private array $choices;
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, mixed $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);
}
@@ -120,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);
@@ -144,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);
@@ -160,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 40eab2429..951d68140 100644
--- a/Question/ConfirmationQuestion.php
+++ b/Question/ConfirmationQuestion.php
@@ -18,18 +18,18 @@
*/
class ConfirmationQuestion extends Question
{
- private string $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 c79683cd5..46a60c798 100644
--- a/Question/Question.php
+++ b/Question/Question.php
@@ -21,13 +21,11 @@
*/
class Question
{
- private string $question;
private ?int $attempts = null;
private bool $hidden = false;
private bool $hiddenFallback = true;
private ?\Closure $autocompleterCallback = null;
private ?\Closure $validator = null;
- private string|int|bool|null|float $default;
private ?\Closure $normalizer = null;
private bool $trimmable = true;
private bool $multiline = false;
@@ -36,10 +34,10 @@ class Question
* @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, string|bool|int|float $default = null)
- {
- $this->question = $question;
- $this->default = $default;
+ public function __construct(
+ private string $question,
+ private string|bool|int|float|null $default = null,
+ ) {
}
/**
diff --git a/Resources/completion.bash b/Resources/completion.bash
index 0d76eacc3..2befe76cb 100644
--- a/Resources/completion.bash
+++ b/Resources/completion.bash
@@ -17,7 +17,7 @@ _sf_{{ COMMAND_NAME }}() {
done
# Use newline as only separator to allow space in completion values
- IFS=$'\n'
+ local IFS=$'\n'
local sf_cmd="${COMP_WORDS[0]}"
# for an alias, get the real script behind it
@@ -37,7 +37,7 @@ _sf_{{ COMMAND_NAME }}() {
local completecmd=("$sf_cmd" "_complete" "--no-interaction" "-sbash" "-c$cword" "-a{{ VERSION }}")
for w in ${words[@]}; do
- w=$(printf -- '%b' "$w")
+ w="${w//\\\\/\\}"
# remove quotes from typed values
quote="${w:0:1}"
if [ "$quote" == \' ]; then
diff --git a/Resources/completion.fish b/Resources/completion.fish
index 1c34292ae..1853dd80f 100644
--- a/Resources/completion.fish
+++ b/Resources/completion.fish
@@ -19,11 +19,7 @@ function _sf_{{ COMMAND_NAME }}
set completecmd $completecmd "-c$c"
- set sfcomplete ($completecmd)
-
- for i in $sfcomplete
- echo $i
- end
+ $completecmd
end
complete -c '{{ COMMAND_NAME }}' -a '(_sf_{{ COMMAND_NAME }})' -f
diff --git a/SignalRegistry/SignalMap.php b/SignalRegistry/SignalMap.php
index de419bda7..2f9aa67c1 100644
--- a/SignalRegistry/SignalMap.php
+++ b/SignalRegistry/SignalMap.php
@@ -27,7 +27,7 @@ public static function getSignalName(int $signal): ?string
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_'), \ARRAY_FILTER_USE_KEY);
+ $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);
}
diff --git a/SignalRegistry/SignalRegistry.php b/SignalRegistry/SignalRegistry.php
index ef2e5f04e..8c2939eec 100644
--- a/SignalRegistry/SignalRegistry.php
+++ b/SignalRegistry/SignalRegistry.php
@@ -54,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 4f0b5ba3c..2b54fb870 100644
--- a/SingleCommandApplication.php
+++ b/SingleCommandApplication.php
@@ -46,7 +46,7 @@ public function setAutoExit(bool $autoExit): static
return $this;
}
- public function run(InputInterface $input = null, OutputInterface $output = null): int
+ public function run(?InputInterface $input = null, ?OutputInterface $output = null): int
{
if ($this->running) {
return parent::run($input, $output);
@@ -67,6 +67,6 @@ public function run(InputInterface $input = null, OutputInterface $output = null
$this->running = false;
}
- return $ret ?? 1;
+ return $ret;
}
}
diff --git a/Style/OutputStyle.php b/Style/OutputStyle.php
index 05076c00f..89a3a4177 100644
--- a/Style/OutputStyle.php
+++ b/Style/OutputStyle.php
@@ -23,11 +23,9 @@
*/
abstract class OutputStyle implements OutputInterface, StyleInterface
{
- private OutputInterface $output;
-
- public function __construct(OutputInterface $output)
- {
- $this->output = $output;
+ public function __construct(
+ private OutputInterface $output,
+ ) {
}
public function newLine(int $count = 1): void
@@ -80,6 +78,12 @@ public function getFormatter(): OutputFormatterInterface
return $this->output->getFormatter();
}
+ 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();
diff --git a/Style/StyleInterface.php b/Style/StyleInterface.php
index 869b16090..fcc5bc775 100644
--- a/Style/StyleInterface.php
+++ b/Style/StyleInterface.php
@@ -71,12 +71,12 @@ public function table(array $headers, array $rows): void;
/**
* Asks a question.
*/
- public function ask(string $question, string $default = null, callable $validator = null): mixed;
+ public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed;
/**
* Asks a question with the user input hidden.
*/
- public function askHidden(string $question, callable $validator = null): mixed;
+ public function askHidden(string $question, ?callable $validator = null): mixed;
/**
* Asks for confirmation.
diff --git a/Style/SymfonyStyle.php b/Style/SymfonyStyle.php
index 0da5d6981..d0788e88d 100644
--- a/Style/SymfonyStyle.php
+++ b/Style/SymfonyStyle.php
@@ -21,6 +21,9 @@
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableCell;
use Symfony\Component\Console\Helper\TableSeparator;
+use Symfony\Component\Console\Helper\TreeHelper;
+use Symfony\Component\Console\Helper\TreeNode;
+use Symfony\Component\Console\Helper\TreeStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
@@ -40,28 +43,27 @@ class SymfonyStyle extends OutputStyle
{
public const MAX_LINE_LENGTH = 120;
- private InputInterface $input;
- private OutputInterface $output;
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.
*/
- public function block(string|array $messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true): void
+ 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];
@@ -74,8 +76,8 @@ 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();
}
@@ -84,8 +86,8 @@ 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();
}
@@ -93,7 +95,7 @@ public function section(string $message): void
public function listing(array $elements): void
{
$this->autoPrependText();
- $elements = array_map(fn ($element) => sprintf(' * %s', $element), $elements);
+ $elements = array_map(fn ($element) => \sprintf(' * %s', $element), $elements);
$this->writeln($elements);
$this->newLine();
@@ -105,7 +107,7 @@ public function text(string|array $message): void
$messages = \is_array($message) ? array_values($message) : [$message];
foreach ($messages as $message) {
- $this->writeln(sprintf(' %s', $message));
+ $this->writeln(\sprintf(' %s', $message));
}
}
@@ -209,7 +211,7 @@ public function definitionList(string|array|TableSeparator ...$list): void
$this->horizontalTable($headers, [$row]);
}
- public function ask(string $question, string $default = null, callable $validator = null): mixed
+ public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed
{
$question = new Question($question, $default);
$question->setValidator($validator);
@@ -217,7 +219,7 @@ public function ask(string $question, string $default = null, callable $validato
return $this->askQuestion($question);
}
- public function askHidden(string $question, callable $validator = null): mixed
+ public function askHidden(string $question, ?callable $validator = null): mixed
{
$question = new Question($question);
@@ -287,7 +289,7 @@ public function createProgressBar(int $max = 0): ProgressBar
*
* @return iterable
*/
- public function progressIterate(iterable $iterable, int $max = null): iterable
+ public function progressIterate(iterable $iterable, ?int $max = null): iterable
{
yield from $this->createProgressBar()->iterate($iterable, $max);
@@ -370,6 +372,24 @@ private function getProgressBar(): ProgressBar
?? throw new RuntimeException('The ProgressBar is not started.');
}
+ /**
+ * @param iterable $nodes
+ */
+ public function tree(iterable $nodes, string $root = ''): void
+ {
+ $this->createTree($nodes, $root)->render();
+ }
+
+ /**
+ * @param iterable $nodes
+ */
+ public function createTree(iterable $nodes, string $root = ''): TreeHelper
+ {
+ $output = $this->output instanceof ConsoleOutputInterface ? $this->output->section() : $this->output;
+
+ return TreeHelper::createTree($output, $root, $nodes, TreeStyle::default());
+ }
+
private function autoPrependBlock(): void
{
$chars = substr(str_replace(\PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2);
@@ -398,14 +418,14 @@ private function writeBuffer(string $message, bool $newLine, int $type): void
$this->bufferedOutput->write($message, $newLine, $type);
}
- private function createBlock(iterable $messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array
+ private function createBlock(iterable $messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array
{
$indentLength = 0;
$prefixLength = Helper::width(Helper::removeDecoration($this->getFormatter(), $prefix));
$lines = [];
if (null !== $type) {
- $type = sprintf('[%s] ', $type);
+ $type = \sprintf('[%s] ', $type);
$indentLength = Helper::width($type);
$lineIndentation = str_repeat(' ', $indentLength);
}
@@ -447,7 +467,7 @@ private function createBlock(iterable $messages, string $type = null, string $st
$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 3eda0376b..80f254434 100644
--- a/Terminal.php
+++ b/Terminal.php
@@ -140,7 +140,7 @@ private static function initDimensions(): void
// 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();
@@ -154,14 +154,6 @@ private static function initDimensions(): void
}
}
- /**
- * 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.
*/
@@ -217,8 +209,7 @@ private static function readFromProcess(string|array $command): ?string
$cp = \function_exists('sapi_windows_cp_set') ? sapi_windows_cp_get() : 0;
- $process = proc_open($command, $descriptorspec, $pipes, null, null, ['suppress_errors' => true]);
- if (!\is_resource($process)) {
+ if (!$process = @proc_open($command, $descriptorspec, $pipes, null, null, ['suppress_errors' => true])) {
return null;
}
diff --git a/Tester/ApplicationTester.php b/Tester/ApplicationTester.php
index 58aee54d6..a6dc8e1ce 100644
--- a/Tester/ApplicationTester.php
+++ b/Tester/ApplicationTester.php
@@ -28,11 +28,9 @@ class ApplicationTester
{
use TesterTrait;
- private Application $application;
-
- public function __construct(Application $application)
- {
- $this->application = $application;
+ public function __construct(
+ private Application $application,
+ ) {
}
/**
@@ -49,37 +47,17 @@ public function __construct(Application $application)
*/
public function run(array $input, array $options = []): int
{
- $prevShellVerbosity = getenv('SHELL_VERBOSITY');
-
- try {
- $this->input = new ArrayInput($input);
- if (isset($options['interactive'])) {
- $this->input->setInteractive($options['interactive']);
- }
+ $this->input = new ArrayInput($input);
+ if (isset($options['interactive'])) {
+ $this->input->setInteractive($options['interactive']);
+ }
- if ($this->inputs) {
- $this->input->setStream(self::createStream($this->inputs));
- }
+ if ($this->inputs) {
+ $this->input->setStream(self::createStream($this->inputs));
+ }
- $this->initOutput($options);
+ $this->initOutput($options);
- return $this->statusCode = $this->application->run($this->input, $this->output);
- } finally {
- // SHELL_VERBOSITY is set by Application::configureIO so we need to unset/reset it
- // to its previous value to avoid one test's verbosity to spread to the following tests
- if (false === $prevShellVerbosity) {
- if (\function_exists('putenv')) {
- @putenv('SHELL_VERBOSITY');
- }
- unset($_ENV['SHELL_VERBOSITY']);
- unset($_SERVER['SHELL_VERBOSITY']);
- } else {
- if (\function_exists('putenv')) {
- @putenv('SHELL_VERBOSITY='.$prevShellVerbosity);
- }
- $_ENV['SHELL_VERBOSITY'] = $prevShellVerbosity;
- $_SERVER['SHELL_VERBOSITY'] = $prevShellVerbosity;
- }
- }
+ return $this->statusCode = $this->application->run($this->input, $this->output);
}
}
diff --git a/Tester/CommandCompletionTester.php b/Tester/CommandCompletionTester.php
index a90fe52ef..76cbaf14f 100644
--- a/Tester/CommandCompletionTester.php
+++ b/Tester/CommandCompletionTester.php
@@ -22,11 +22,9 @@
*/
class CommandCompletionTester
{
- private Command $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 2ff813b7d..d39cde7f6 100644
--- a/Tester/CommandTester.php
+++ b/Tester/CommandTester.php
@@ -24,11 +24,9 @@ class CommandTester
{
use TesterTrait;
- private Command $command;
-
- public function __construct(Command $command)
- {
- $this->command = $command;
+ public function __construct(
+ private Command $command,
+ ) {
}
/**
diff --git a/Tester/Constraint/CommandIsSuccessful.php b/Tester/Constraint/CommandIsSuccessful.php
index 09c6194b9..d677c27aa 100644
--- a/Tester/Constraint/CommandIsSuccessful.php
+++ b/Tester/Constraint/CommandIsSuccessful.php
@@ -38,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 1ab7a70aa..238c7b7eb 100644
--- a/Tester/TesterTrait.php
+++ b/Tester/TesterTrait.php
@@ -168,7 +168,11 @@ private static function createStream(array $inputs)
$stream = fopen('php://memory', 'r+', false);
foreach ($inputs as $input) {
- fwrite($stream, $input.\PHP_EOL);
+ fwrite($stream, $input);
+
+ if (!str_ends_with($input, "\x4")) {
+ fwrite($stream, \PHP_EOL);
+ }
}
rewind($stream);
diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php
index ca85c24b1..6390d4828 100644
--- a/Tests/ApplicationTest.php
+++ b/Tests/ApplicationTest.php
@@ -22,6 +22,7 @@
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;
@@ -36,6 +37,7 @@
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\Output;
@@ -44,6 +46,7 @@
use Symfony\Component\Console\SignalRegistry\SignalRegistry;
use Symfony\Component\Console\Terminal;
use Symfony\Component\Console\Tester\ApplicationTester;
+use Symfony\Component\Console\Tests\Fixtures\MockableAppliationWithTerminalWidth;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -69,13 +72,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);
}
}
@@ -192,8 +197,10 @@ public function testRegister()
public function testRegisterAmbiguous()
{
- $code = function (InputInterface $input, OutputInterface $output) {
+ $code = function (InputInterface $input, OutputInterface $output): int {
$output->writeln('It works!');
+
+ return 0;
};
$application = new Application();
@@ -287,7 +294,7 @@ public function testSilentHelp()
$tester = new ApplicationTester($application);
$tester->run(['-h' => true, '-q' => true], ['decorated' => false]);
- $this->assertEmpty($tester->getDisplay(true));
+ $this->assertSame('', $tester->getDisplay(true));
}
public function testGetInvalidCommand()
@@ -636,7 +643,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
@@ -647,7 +654,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');
@@ -825,7 +832,7 @@ public function testSetCatchErrors(bool $catchExceptions)
try {
$tester->run(['command' => 'boom']);
- $this->fail('The exception is not catched.');
+ $this->fail('The exception is not caught.');
} catch (\Throwable $e) {
$this->assertInstanceOf(\Error::class, $e);
$this->assertSame('This is an error.', $e->getMessage());
@@ -852,12 +859,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');
@@ -932,7 +942,9 @@ public function testRenderExceptionEscapesLines()
public function testRenderExceptionLineBreaks()
{
- $application = $this->getMockBuilder(Application::class)->addMethods(['getTerminalWidth'])->getMock();
+ $application = $this->getMockBuilder(MockableAppliationWithTerminalWidth::class)
+ ->onlyMethods(['getTerminalWidth'])
+ ->getMock();
$application->setAutoExit(false);
$application->expects($this->any())
->method('getTerminalWidth')
@@ -964,7 +976,7 @@ public function testRenderAnonymousException()
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
- throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', (new class() {})::class));
+ throw new \InvalidArgumentException(\sprintf('Dummy type "%s" is invalid.', (new class {})::class));
});
$tester = new ApplicationTester($application);
@@ -990,7 +1002,7 @@ public function testRenderExceptionStackTraceContainsRootException()
$application = new Application();
$application->setAutoExit(false);
$application->register('foo')->setCode(function () {
- throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', (new class() {})::class));
+ throw new \InvalidArgumentException(\sprintf('Dummy type "%s" is invalid.', (new class {})::class));
});
$tester = new ApplicationTester($application);
@@ -1266,7 +1278,9 @@ public function testAddingOptionWithDuplicateShortcut()
->register('foo')
->setAliases(['f'])
->setDefinition([new InputOption('survey', 'e', InputOption::VALUE_REQUIRED, 'My option with a shortcut.')])
- ->setCode(function (InputInterface $input, OutputInterface $output) {})
+ ->setCode(function (InputInterface $input, OutputInterface $output): int {
+ return 0;
+ })
;
$input = new ArrayInput(['command' => 'foo']);
@@ -1289,7 +1303,9 @@ public function testAddingAlreadySetDefinitionElementData($def)
$application
->register('foo')
->setDefinition([$def])
- ->setCode(function (InputInterface $input, OutputInterface $output) {})
+ ->setCode(function (InputInterface $input, OutputInterface $output): int {
+ return 0;
+ })
;
$input = new ArrayInput(['command' => 'foo']);
@@ -1426,8 +1442,10 @@ public function testRunWithDispatcher()
$application->setAutoExit(false);
$application->setDispatcher($this->getDispatcher());
- $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');
+
+ return 0;
});
$tester = new ApplicationTester($application);
@@ -1482,8 +1500,10 @@ public function testRunDispatchesAllEventsWithExceptionInListener()
$application->setDispatcher($dispatcher);
$application->setAutoExit(false);
- $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');
+
+ return 0;
});
$tester = new ApplicationTester($application);
@@ -1550,8 +1570,10 @@ public function testRunAllowsErrorListenersToSilenceTheException()
$application->setDispatcher($dispatcher);
$application->setAutoExit(false);
- $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');
+
+ return 0;
});
$tester = new ApplicationTester($application);
@@ -1662,8 +1684,10 @@ public function testRunWithDispatcherSkippingCommand()
$application->setDispatcher($this->getDispatcher(true));
$application->setAutoExit(false);
- $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');
+
+ return 0;
});
$tester = new ApplicationTester($application);
@@ -1689,8 +1713,10 @@ public function testRunWithDispatcherAccessingInputOptions()
$application->setDispatcher($dispatcher);
$application->setAutoExit(false);
- $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');
+
+ return 0;
});
$tester = new ApplicationTester($application);
@@ -1719,8 +1745,10 @@ public function testRunWithDispatcherAddingInputOptions()
$application->setDispatcher($dispatcher);
$application->setAutoExit(false);
- $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');
+
+ return 0;
});
$tester = new ApplicationTester($application);
@@ -1849,12 +1877,12 @@ public function testFindAlternativesDoesNotLoadSameNamespaceCommandsOnExactMatch
'foo:bar' => function () use (&$loaded) {
$loaded['foo:bar'] = true;
- return (new Command('foo:bar'))->setCode(function () {});
+ return (new Command('foo:bar'))->setCode(function (): int { return 0; });
},
'foo' => function () use (&$loaded) {
$loaded['foo'] = true;
- return (new Command('foo'))->setCode(function () {});
+ return (new Command('foo'))->setCode(function (): int { return 0; });
},
]));
@@ -1925,8 +1953,10 @@ public function testThrowingErrorListener()
$application->setAutoExit(false);
$application->setCatchExceptions(false);
- $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->write('foo.');
+
+ return 0;
});
$tester = new ApplicationTester($application);
@@ -2226,6 +2256,203 @@ public function testSignalableRestoresStty()
$this->assertSame($previousSttyMode, $sttyMode);
}
+ /**
+ * @requires extension pcntl
+ */
+ public function testSignalableInvokableCommand()
+ {
+ $command = new Command();
+ $command->setName('signal-invokable');
+ $command->setCode($invokable = new class implements SignalableCommandInterface {
+ use SignalableInvokableCommandTrait;
+ });
+
+ $application = $this->createSignalableApplication($command, null);
+ $application->setSignalsToDispatchEvent(\SIGUSR1);
+
+ $this->assertSame(1, $application->run(new ArrayInput(['signal-invokable'])));
+ $this->assertTrue($invokable->signaled);
+ }
+
+ /**
+ * @requires extension pcntl
+ */
+ public function testSignalableInvokableCommandThatExtendsBaseCommand()
+ {
+ $command = new class extends Command implements SignalableCommandInterface {
+ use SignalableInvokableCommandTrait;
+ };
+ $command->setName('signal-invokable');
+
+ $application = $this->createSignalableApplication($command, null);
+ $application->setSignalsToDispatchEvent(\SIGUSR1);
+
+ $this->assertSame(1, $application->run(new ArrayInput(['signal-invokable'])));
+ $this->assertTrue($command->signaled);
+ }
+
+ /**
+ * @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();
@@ -2233,10 +2460,106 @@ private function createSignalableApplication(Command $command, ?EventDispatcherI
if ($dispatcher) {
$application->setDispatcher($dispatcher);
}
- $application->add(new LazyCommand('signal', [], '', false, fn () => $command, true));
+ $application->add(new LazyCommand($command->getName(), [], '', false, fn () => $command, true));
return $application;
}
+
+ public function testShellVerbosityIsRestoredAfterCommandExecutionWithInitialValue()
+ {
+ // Set initial SHELL_VERBOSITY
+ putenv('SHELL_VERBOSITY=-2');
+ $_ENV['SHELL_VERBOSITY'] = '-2';
+ $_SERVER['SHELL_VERBOSITY'] = '-2';
+
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->register('foo')
+ ->setCode(function (InputInterface $input, OutputInterface $output): int {
+ $output->write('SHELL_VERBOSITY: '.$_SERVER['SHELL_VERBOSITY']);
+
+ return 0;
+ });
+
+ $input = new ArrayInput(['command' => 'foo', '--verbose' => 3]);
+ $output = new BufferedOutput();
+
+ $application->run($input, $output);
+
+ $this->assertSame('SHELL_VERBOSITY: 3', $output->fetch());
+ $this->assertSame('-2', getenv('SHELL_VERBOSITY'));
+ $this->assertSame('-2', $_ENV['SHELL_VERBOSITY']);
+ $this->assertSame('-2', $_SERVER['SHELL_VERBOSITY']);
+
+ // Clean up for other tests
+ putenv('SHELL_VERBOSITY');
+ unset($_ENV['SHELL_VERBOSITY']);
+ unset($_SERVER['SHELL_VERBOSITY']);
+ }
+
+ public function testShellVerbosityIsRemovedAfterCommandExecutionWhenNotSetInitially()
+ {
+ // Ensure SHELL_VERBOSITY is not set initially
+ putenv('SHELL_VERBOSITY');
+ unset($_ENV['SHELL_VERBOSITY']);
+ unset($_SERVER['SHELL_VERBOSITY']);
+
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->register('foo')
+ ->setCode(function (InputInterface $input, OutputInterface $output): int {
+ $output->write('SHELL_VERBOSITY: '.$_SERVER['SHELL_VERBOSITY']);
+
+ return 0;
+ });
+
+ $input = new ArrayInput(['command' => 'foo', '--verbose' => 3]);
+ $output = new BufferedOutput();
+
+ $application->run($input, $output);
+
+ $this->assertSame('SHELL_VERBOSITY: 3', $output->fetch());
+ $this->assertFalse(getenv('SHELL_VERBOSITY'));
+ $this->assertArrayNotHasKey('SHELL_VERBOSITY', $_ENV);
+ $this->assertArrayNotHasKey('SHELL_VERBOSITY', $_SERVER);
+ }
+
+ public function testShellVerbosityDoesNotLeakBetweenCommandExecutions()
+ {
+ // Ensure no initial SHELL_VERBOSITY
+ putenv('SHELL_VERBOSITY');
+ unset($_ENV['SHELL_VERBOSITY']);
+ unset($_SERVER['SHELL_VERBOSITY']);
+
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->register('verbose-cmd')
+ ->setCode(function (InputInterface $input, OutputInterface $output): int {
+ $output->write('SHELL_VERBOSITY: '.$_SERVER['SHELL_VERBOSITY']);
+
+ return 0;
+ });
+ $application->register('normal-cmd')
+ ->setCode(function (InputInterface $input, OutputInterface $output): int {
+ $output->write('SHELL_VERBOSITY: '.$_SERVER['SHELL_VERBOSITY']);
+
+ return 0;
+ });
+
+ $output = new BufferedOutput();
+
+ $application->run(new ArrayInput(['command' => 'verbose-cmd', '--verbose' => true]), $output);
+
+ $this->assertSame('SHELL_VERBOSITY: 1', $output->fetch(), 'SHELL_VERBOSITY should be set to 1 for verbose command');
+ $this->assertFalse(getenv('SHELL_VERBOSITY'), 'SHELL_VERBOSITY should not be set after first command');
+
+ $application->run(new ArrayInput(['command' => 'normal-cmd']), $output);
+
+ $this->assertSame('SHELL_VERBOSITY: 0', $output->fetch(), 'SHELL_VERBOSITY should not leak to second command');
+ $this->assertFalse(getenv('SHELL_VERBOSITY'), 'SHELL_VERBOSITY should not leak to second command');
+ $this->assertArrayNotHasKey('SHELL_VERBOSITY', $_ENV);
+ $this->assertArrayNotHasKey('SHELL_VERBOSITY', $_SERVER);
+ }
}
class CustomApplication extends Application
@@ -2323,7 +2646,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
#[AsCommand(name: 'signal')]
-class SignableCommand extends BaseSignableCommand implements SignalableCommandInterface
+class SignableCommand extends BaseSignableCommand
{
public function getSubscribedSignals(): array
{
@@ -2340,7 +2663,7 @@ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|
}
#[AsCommand(name: 'signal')]
-class TerminatableCommand extends BaseSignableCommand implements SignalableCommandInterface
+class TerminatableCommand extends BaseSignableCommand
{
public function getSubscribedSignals(): array
{
@@ -2357,7 +2680,7 @@ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|
}
#[AsCommand(name: 'signal')]
-class TerminatableWithEventCommand extends Command implements SignalableCommandInterface, EventSubscriberInterface
+class TerminatableWithEventCommand extends Command implements EventSubscriberInterface
{
private bool $shouldContinue = true;
private OutputInterface $output;
@@ -2423,3 +2746,82 @@ public static function getSubscribedEvents(): array
return ['console.signal' => 'onSignal'];
}
}
+
+trait SignalableInvokableCommandTrait
+{
+ public bool $signaled = false;
+
+ public function __invoke(): int
+ {
+ posix_kill(posix_getpid(), \SIGUSR1);
+
+ for ($i = 0; $i < 1000; ++$i) {
+ usleep(100);
+ if ($this->signaled) {
+ return 1;
+ }
+ }
+
+ return 0;
+ }
+
+ public function getSubscribedSignals(): array
+ {
+ return SignalRegistry::isSupported() ? [\SIGUSR1] : [];
+ }
+
+ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
+ {
+ $this->signaled = true;
+
+ return false;
+ }
+}
+
+#[AsCommand(name: 'alarm')]
+class AlarmableCommand extends BaseSignableCommand
+{
+ 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/Command/CommandTest.php b/Tests/Command/CommandTest.php
index f3e4b51d1..0db3572fc 100644
--- a/Tests/Command/CommandTest.php
+++ b/Tests/Command/CommandTest.php
@@ -12,11 +12,13 @@
namespace Symfony\Component\Console\Tests\Command;
use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\PhpUnit\ExpectUserDeprecationMessageTrait;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Helper\FormatterHelper;
+use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
@@ -28,6 +30,8 @@
class CommandTest extends TestCase
{
+ use ExpectUserDeprecationMessageTrait;
+
protected static string $fixturesPath;
public static function setUpBeforeClass(): void
@@ -139,7 +143,7 @@ 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));
(new \TestCommand())->setName($name);
}
@@ -347,8 +351,10 @@ public function testRunWithProcessTitle()
public function testSetCode()
{
$command = new \TestCommand();
- $ret = $command->setCode(function (InputInterface $input, OutputInterface $output) {
+ $ret = $command->setCode(function (InputInterface $input, OutputInterface $output): int {
$output->writeln('from the code...');
+
+ return 0;
});
$this->assertEquals($command, $ret, '->setCode() implements a fluent interface');
$tester = new CommandTester($command);
@@ -393,8 +399,10 @@ public function testSetCodeWithStaticClosure()
private static function createClosure()
{
- return function (InputInterface $input, OutputInterface $output) {
+ return function (InputInterface $input, OutputInterface $output): int {
$output->writeln(isset($this) ? 'bound' : 'not bound');
+
+ return 0;
};
}
@@ -408,16 +416,20 @@ public function testSetCodeWithNonClosureCallable()
$this->assertEquals('interact called'.\PHP_EOL.'from the code...'.\PHP_EOL, $tester->getDisplay());
}
- public function callableMethodCommand(InputInterface $input, OutputInterface $output)
+ public function callableMethodCommand(InputInterface $input, OutputInterface $output): int
{
$output->writeln('from the code...');
+
+ return 0;
}
public function testSetCodeWithStaticAnonymousFunction()
{
$command = new \TestCommand();
- $command->setCode(static function (InputInterface $input, OutputInterface $output) {
+ $command->setCode(static function (InputInterface $input, OutputInterface $output): int {
$output->writeln(isset($this) ? 'bound' : 'not bound');
+
+ return 0;
});
$tester = new CommandTester($command);
$tester->execute([]);
@@ -427,53 +439,97 @@ public function testSetCodeWithStaticAnonymousFunction()
public function testCommandAttribute()
{
- $this->assertSame('|foo|f', Php8Command::getDefaultName());
- $this->assertSame('desc', Php8Command::getDefaultDescription());
-
$command = new Php8Command();
$this->assertSame('foo', $command->getName());
$this->assertSame('desc', $command->getDescription());
+ $this->assertSame('help', $command->getHelp());
$this->assertTrue($command->isHidden());
$this->assertSame(['f'], $command->getAliases());
}
- public function testAttributeOverridesProperty()
+ /**
+ * @group legacy
+ */
+ public function testCommandAttributeWithDeprecatedMethods()
{
- $this->assertSame('my:command', MyAnnotatedCommand::getDefaultName());
- $this->assertSame('This is a command I wrote all by myself', MyAnnotatedCommand::getDefaultDescription());
+ $this->expectUserDeprecationMessage('Since symfony/console 7.3: Method "Symfony\Component\Console\Command\Command::getDefaultName()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.');
+ $this->expectUserDeprecationMessage('Since symfony/console 7.3: Method "Symfony\Component\Console\Command\Command::getDefaultDescription()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.');
+
+ $this->assertSame('|foo|f', Php8Command::getDefaultName());
+ $this->assertSame('desc', Php8Command::getDefaultDescription());
+ }
+ public function testAttributeOverridesProperty()
+ {
$command = new MyAnnotatedCommand();
$this->assertSame('my:command', $command->getName());
$this->assertSame('This is a command I wrote all by myself', $command->getDescription());
}
+ /**
+ * @group legacy
+ */
+ public function testAttributeOverridesPropertyWithDeprecatedMethods()
+ {
+ $this->expectUserDeprecationMessage('Since symfony/console 7.3: Method "Symfony\Component\Console\Command\Command::getDefaultName()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.');
+ $this->expectUserDeprecationMessage('Since symfony/console 7.3: Method "Symfony\Component\Console\Command\Command::getDefaultDescription()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.');
+
+ $this->assertSame('my:command', MyAnnotatedCommand::getDefaultName());
+ $this->assertSame('This is a command I wrote all by myself', MyAnnotatedCommand::getDefaultDescription());
+ }
+
public function testDefaultCommand()
{
$apl = new Application();
- $apl->setDefaultCommand(Php8Command::getDefaultName());
+ $apl->setDefaultCommand('foo');
$property = new \ReflectionProperty($apl, 'defaultCommand');
$this->assertEquals('foo', $property->getValue($apl));
- $apl->setDefaultCommand(Php8Command2::getDefaultName());
+ $apl->setDefaultCommand('foo2');
$property = new \ReflectionProperty($apl, 'defaultCommand');
$this->assertEquals('foo2', $property->getValue($apl));
}
+
+ /**
+ * @group legacy
+ */
+ public function testDeprecatedMethods()
+ {
+ $this->expectUserDeprecationMessage('Since symfony/console 7.3: Overriding "Command::getDefaultName()" in "Symfony\Component\Console\Tests\Command\FooCommand" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.');
+ $this->expectUserDeprecationMessage('Since symfony/console 7.3: Overriding "Command::getDefaultDescription()" in "Symfony\Component\Console\Tests\Command\FooCommand" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.');
+
+ new FooCommand();
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testDeprecatedNonIntegerReturnTypeFromClosureCode()
+ {
+ $this->expectUserDeprecationMessage('Since symfony/console 7.3: Returning a non-integer value from the command "foo" is deprecated and will throw an exception in Symfony 8.0.');
+
+ $command = new Command('foo');
+ $command->setCode(function () {});
+ $command->run(new ArrayInput([]), new NullOutput());
+ }
}
// In order to get an unbound closure, we should create it outside a class
// scope.
function createClosure()
{
- return function (InputInterface $input, OutputInterface $output) {
+ return function (InputInterface $input, OutputInterface $output): int {
$output->writeln($this instanceof Command ? 'bound to the command' : 'not bound to the command');
+
+ return 0;
};
}
-#[AsCommand(name: 'foo', description: 'desc', hidden: true, aliases: ['f'])]
+#[AsCommand(name: 'foo', description: 'desc', hidden: true, aliases: ['f'], help: 'help')]
class Php8Command extends Command
{
}
@@ -490,3 +546,16 @@ class MyAnnotatedCommand extends Command
protected static $defaultDescription = 'This description should be ignored.';
}
+
+class FooCommand extends Command
+{
+ public static function getDefaultName(): ?string
+ {
+ return 'foo';
+ }
+
+ public static function getDefaultDescription(): ?string
+ {
+ return 'foo description';
+ }
+}
diff --git a/Tests/Command/CompleteCommandTest.php b/Tests/Command/CompleteCommandTest.php
index 0f64fbc8e..75519eb49 100644
--- a/Tests/Command/CompleteCommandTest.php
+++ b/Tests/Command/CompleteCommandTest.php
@@ -53,6 +53,8 @@ public function testUnsupportedShellOption()
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,9 +119,9 @@ 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']];
}
diff --git a/Tests/Command/InvokableCommandTest.php b/Tests/Command/InvokableCommandTest.php
new file mode 100644
index 000000000..9fc40809a
--- /dev/null
+++ b/Tests/Command/InvokableCommandTest.php
@@ -0,0 +1,383 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Tests\Command;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Attribute\Argument;
+use Symfony\Component\Console\Attribute\Option;
+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\InvalidOptionException;
+use Symfony\Component\Console\Exception\LogicException;
+use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\NullOutput;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class InvokableCommandTest extends TestCase
+{
+ public function testCommandInputArgumentDefinition()
+ {
+ $command = new Command('foo');
+ $command->setCode(function (
+ #[Argument(name: 'very-first-name')] string $name,
+ #[Argument] ?string $firstName,
+ #[Argument] string $lastName = '',
+ #[Argument(description: 'Short argument description')] string $bio = '',
+ #[Argument(suggestedValues: [self::class, 'getSuggestedRoles'])] array $roles = ['ROLE_USER'],
+ ): int {
+ return 0;
+ });
+
+ $nameInputArgument = $command->getDefinition()->getArgument('very-first-name');
+ self::assertSame('very-first-name', $nameInputArgument->getName());
+ self::assertTrue($nameInputArgument->isRequired());
+
+ $lastNameInputArgument = $command->getDefinition()->getArgument('first-name');
+ self::assertSame('first-name', $lastNameInputArgument->getName());
+ self::assertFalse($lastNameInputArgument->isRequired());
+ self::assertNull($lastNameInputArgument->getDefault());
+
+ $lastNameInputArgument = $command->getDefinition()->getArgument('last-name');
+ self::assertSame('last-name', $lastNameInputArgument->getName());
+ self::assertFalse($lastNameInputArgument->isRequired());
+ self::assertSame('', $lastNameInputArgument->getDefault());
+
+ $bioInputArgument = $command->getDefinition()->getArgument('bio');
+ self::assertSame('bio', $bioInputArgument->getName());
+ self::assertFalse($bioInputArgument->isRequired());
+ self::assertSame('Short argument description', $bioInputArgument->getDescription());
+ self::assertSame('', $bioInputArgument->getDefault());
+
+ $rolesInputArgument = $command->getDefinition()->getArgument('roles');
+ self::assertSame('roles', $rolesInputArgument->getName());
+ self::assertFalse($rolesInputArgument->isRequired());
+ self::assertTrue($rolesInputArgument->isArray());
+ self::assertSame(['ROLE_USER'], $rolesInputArgument->getDefault());
+ self::assertTrue($rolesInputArgument->hasCompletion());
+ $rolesInputArgument->complete(new CompletionInput(), $suggestions = new CompletionSuggestions());
+ self::assertSame(['ROLE_ADMIN', 'ROLE_USER'], array_map(static fn (Suggestion $s) => $s->getValue(), $suggestions->getValueSuggestions()));
+ }
+
+ public function testCommandInputOptionDefinition()
+ {
+ $command = new Command('foo');
+ $command->setCode(function (
+ #[Option(name: 'idle')] ?int $timeout = null,
+ #[Option] string $type = 'USER_TYPE',
+ #[Option(shortcut: 'v')] bool $verbose = false,
+ #[Option(description: 'User groups')] array $groups = [],
+ #[Option(suggestedValues: [self::class, 'getSuggestedRoles'])] array $roles = ['ROLE_USER'],
+ #[Option] string|bool $opt = false,
+ ): int {
+ return 0;
+ });
+
+ $timeoutInputOption = $command->getDefinition()->getOption('idle');
+ self::assertSame('idle', $timeoutInputOption->getName());
+ self::assertNull($timeoutInputOption->getShortcut());
+ self::assertTrue($timeoutInputOption->isValueRequired());
+ self::assertFalse($timeoutInputOption->isValueOptional());
+ self::assertFalse($timeoutInputOption->isNegatable());
+ self::assertNull($timeoutInputOption->getDefault());
+
+ $typeInputOption = $command->getDefinition()->getOption('type');
+ self::assertSame('type', $typeInputOption->getName());
+ self::assertTrue($typeInputOption->isValueRequired());
+ self::assertFalse($typeInputOption->isNegatable());
+ self::assertSame('USER_TYPE', $typeInputOption->getDefault());
+
+ $verboseInputOption = $command->getDefinition()->getOption('verbose');
+ self::assertSame('verbose', $verboseInputOption->getName());
+ self::assertSame('v', $verboseInputOption->getShortcut());
+ self::assertFalse($verboseInputOption->isValueRequired());
+ self::assertFalse($verboseInputOption->isValueOptional());
+ self::assertFalse($verboseInputOption->isNegatable());
+ self::assertFalse($verboseInputOption->getDefault());
+
+ $groupsInputOption = $command->getDefinition()->getOption('groups');
+ self::assertSame('groups', $groupsInputOption->getName());
+ self::assertTrue($groupsInputOption->isArray());
+ self::assertSame('User groups', $groupsInputOption->getDescription());
+ self::assertFalse($groupsInputOption->isNegatable());
+ self::assertSame([], $groupsInputOption->getDefault());
+
+ $rolesInputOption = $command->getDefinition()->getOption('roles');
+ self::assertSame('roles', $rolesInputOption->getName());
+ self::assertTrue($rolesInputOption->isValueRequired());
+ self::assertFalse($rolesInputOption->isNegatable());
+ self::assertTrue($rolesInputOption->isArray());
+ self::assertSame(['ROLE_USER'], $rolesInputOption->getDefault());
+ self::assertTrue($rolesInputOption->hasCompletion());
+ $rolesInputOption->complete(new CompletionInput(), $suggestions = new CompletionSuggestions());
+ self::assertSame(['ROLE_ADMIN', 'ROLE_USER'], array_map(static fn (Suggestion $s) => $s->getValue(), $suggestions->getValueSuggestions()));
+
+ $optInputOption = $command->getDefinition()->getOption('opt');
+ self::assertSame('opt', $optInputOption->getName());
+ self::assertNull($optInputOption->getShortcut());
+ self::assertFalse($optInputOption->isValueRequired());
+ self::assertTrue($optInputOption->isValueOptional());
+ self::assertFalse($optInputOption->isNegatable());
+ self::assertFalse($optInputOption->getDefault());
+ }
+
+ public function testInvalidArgumentType()
+ {
+ $command = new Command('foo');
+ $command->setCode(function (#[Argument] object $any) {});
+
+ $this->expectException(LogicException::class);
+
+ $command->getDefinition();
+ }
+
+ public function testInvalidOptionType()
+ {
+ $command = new Command('foo');
+ $command->setCode(function (#[Option] ?object $any = null) {});
+
+ $this->expectException(LogicException::class);
+
+ $command->getDefinition();
+ }
+
+ public function testExecuteHasPriorityOverInvokeMethod()
+ {
+ $command = new class extends Command {
+ public string $called;
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $this->called = __FUNCTION__;
+
+ return 0;
+ }
+
+ public function __invoke(): int
+ {
+ $this->called = __FUNCTION__;
+
+ return 0;
+ }
+ };
+
+ $command->run(new ArrayInput([]), new NullOutput());
+ $this->assertSame('execute', $command->called);
+ }
+
+ public function testCallInvokeMethodWhenExtendingCommandClass()
+ {
+ $command = new class extends Command {
+ public string $called;
+
+ public function __invoke(): int
+ {
+ $this->called = __FUNCTION__;
+
+ return 0;
+ }
+ };
+
+ $command->run(new ArrayInput([]), new NullOutput());
+ $this->assertSame('__invoke', $command->called);
+ }
+
+ public function testInvalidReturnType()
+ {
+ $command = new Command('foo');
+ $command->setCode(new class {
+ public function __invoke()
+ {
+ }
+ });
+
+ $this->expectException(\TypeError::class);
+ $this->expectExceptionMessage('The command "foo" must return an integer value in the "__invoke" method, but "null" was returned.');
+
+ $command->run(new ArrayInput([]), new NullOutput());
+ }
+
+ /**
+ * @dataProvider provideInputArguments
+ */
+ public function testInputArguments(array $parameters, array $expected)
+ {
+ $command = new Command('foo');
+ $command->setCode(function (
+ #[Argument] string $a,
+ #[Argument] ?string $b,
+ #[Argument] string $c = '',
+ #[Argument] array $d = [],
+ ) use ($expected): int {
+ $this->assertSame($expected[0], $a);
+ $this->assertSame($expected[1], $b);
+ $this->assertSame($expected[2], $c);
+ $this->assertSame($expected[3], $d);
+
+ return 0;
+ });
+
+ $command->run(new ArrayInput($parameters), new NullOutput());
+ }
+
+ public static function provideInputArguments(): \Generator
+ {
+ yield 'required & defaults' => [['a' => 'x'], ['x', null, '', []]];
+ yield 'required & with-value' => [['a' => 'x', 'b' => 'y', 'c' => 'z', 'd' => ['d']], ['x', 'y', 'z', ['d']]];
+ yield 'required & without-value' => [['a' => 'x', 'b' => null, 'c' => null, 'd' => null], ['x', null, '', []]];
+ }
+
+ /**
+ * @dataProvider provideBinaryInputOptions
+ */
+ public function testBinaryInputOptions(array $parameters, array $expected)
+ {
+ $command = new Command('foo');
+ $command->setCode(function (
+ #[Option] bool $a = true,
+ #[Option] bool $b = false,
+ #[Option] ?bool $c = null,
+ ) use ($expected): int {
+ $this->assertSame($expected[0], $a);
+ $this->assertSame($expected[1], $b);
+ $this->assertSame($expected[2], $c);
+
+ return 0;
+ });
+
+ $command->run(new ArrayInput($parameters), new NullOutput());
+ }
+
+ public static function provideBinaryInputOptions(): \Generator
+ {
+ yield 'defaults' => [[], [true, false, null]];
+ yield 'positive' => [['--a' => null, '--b' => null, '--c' => null], [true, true, true]];
+ yield 'negative' => [['--no-a' => null, '--no-c' => null], [false, false, false]];
+ }
+
+ /**
+ * @dataProvider provideNonBinaryInputOptions
+ */
+ public function testNonBinaryInputOptions(array $parameters, array $expected)
+ {
+ $command = new Command('foo');
+ $command->setCode(function (
+ #[Option] string $a = '',
+ #[Option] array $b = [],
+ #[Option] array $c = ['a', 'b'],
+ #[Option] bool|string $d = false,
+ #[Option] ?string $e = null,
+ #[Option] ?array $f = null,
+ #[Option] int $g = 0,
+ #[Option] ?int $h = null,
+ #[Option] float $i = 0.0,
+ #[Option] ?float $j = null,
+ #[Option] bool|int $k = false,
+ #[Option] bool|float $l = false,
+ ) use ($expected): int {
+ $this->assertSame($expected[0], $a);
+ $this->assertSame($expected[1], $b);
+ $this->assertSame($expected[2], $c);
+ $this->assertSame($expected[3], $d);
+ $this->assertSame($expected[4], $e);
+ $this->assertSame($expected[5], $f);
+ $this->assertSame($expected[6], $g);
+ $this->assertSame($expected[7], $h);
+ $this->assertSame($expected[8], $i);
+ $this->assertSame($expected[9], $j);
+ $this->assertSame($expected[10], $k);
+ $this->assertSame($expected[11], $l);
+
+ return 0;
+ });
+
+ $command->run(new ArrayInput($parameters), new NullOutput());
+ }
+
+ public static function provideNonBinaryInputOptions(): \Generator
+ {
+ yield 'defaults' => [
+ [],
+ ['', [], ['a', 'b'], false, null, null, 0, null, 0.0, null, false, false],
+ ];
+ yield 'with-value' => [
+ ['--a' => 'x', '--b' => ['z'], '--c' => ['c', 'd'], '--d' => 'v', '--e' => 'w', '--f' => ['q'], '--g' => 1, '--h' => 2, '--i' => 3.1, '--j' => 4.2, '--k' => 5, '--l' => 6.3],
+ ['x', ['z'], ['c', 'd'], 'v', 'w', ['q'], 1, 2, 3.1, 4.2, 5, 6.3],
+ ];
+ yield 'without-value' => [
+ ['--d' => null, '--k' => null, '--l' => null],
+ ['', [], ['a', 'b'], true, null, null, 0, null, 0.0, null, true, true],
+ ];
+ }
+
+ /**
+ * @dataProvider provideInvalidOptionDefinitions
+ */
+ public function testInvalidOptionDefinition(callable $code)
+ {
+ $command = new Command('foo');
+ $command->setCode($code);
+
+ $this->expectException(LogicException::class);
+
+ $command->getDefinition();
+ }
+
+ public static function provideInvalidOptionDefinitions(): \Generator
+ {
+ yield 'no-default' => [
+ function (#[Option] string $a) {},
+ ];
+ yield 'nullable-bool-default-true' => [
+ function (#[Option] ?bool $a = true) {},
+ ];
+ yield 'nullable-bool-default-false' => [
+ function (#[Option] ?bool $a = false) {},
+ ];
+ yield 'invalid-union-type' => [
+ function (#[Option] array|bool $a = false) {},
+ ];
+ yield 'union-type-cannot-allow-null' => [
+ function (#[Option] string|bool|null $a = null) {},
+ ];
+ yield 'union-type-default-true' => [
+ function (#[Option] string|bool $a = true) {},
+ ];
+ yield 'union-type-default-string' => [
+ function (#[Option] string|bool $a = 'foo') {},
+ ];
+ yield 'nullable-string-not-null-default' => [
+ function (#[Option] ?string $a = 'foo') {},
+ ];
+ yield 'nullable-array-not-null-default' => [
+ function (#[Option] ?array $a = []) {},
+ ];
+ }
+
+ public function testInvalidRequiredValueOptionEvenWithDefault()
+ {
+ $command = new Command('foo');
+ $command->setCode(function (#[Option] string $a = 'a') {});
+
+ $this->expectException(InvalidOptionException::class);
+ $this->expectExceptionMessage('The "--a" option requires a value.');
+
+ $command->run(new ArrayInput(['--a' => null]), new NullOutput());
+ }
+
+ public function getSuggestedRoles(CompletionInput $input): array
+ {
+ return ['ROLE_ADMIN', 'ROLE_USER'];
+ }
+}
diff --git a/Tests/Command/ListCommandTest.php b/Tests/Command/ListCommandTest.php
index 20dfa8d30..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
diff --git a/Tests/Command/LockableTraitTest.php b/Tests/Command/LockableTraitTest.php
index 77b54f9ee..3000906d7 100644
--- a/Tests/Command/LockableTraitTest.php
+++ b/Tests/Command/LockableTraitTest.php
@@ -12,8 +12,10 @@
namespace Symfony\Component\Console\Tests\Command;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Command\Command;
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;
@@ -26,6 +28,8 @@ 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';
+ require_once self::$fixturesPath.'/FooLock4InvokableCommand.php';
}
public function testLockIsReleased()
@@ -64,4 +68,39 @@ 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([]));
+ }
+
+ public function testLockInvokableCommandReturnsFalseIfAlreadyLockedByAnotherCommand()
+ {
+ $command = new Command('foo:lock4');
+ $command->setCode(new \FooLock4InvokableCommand());
+
+ if (SemaphoreStore::isSupported()) {
+ $store = new SemaphoreStore();
+ } else {
+ $store = new FlockStore();
+ }
+
+ $lock = (new LockFactory($store))->createLock($command->getName());
+ $lock->acquire();
+
+ $tester = new CommandTester($command);
+ $this->assertSame(Command::FAILURE, $tester->execute([]));
+
+ $lock->release();
+ $this->assertSame(Command::SUCCESS, $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/Command/TraceableCommandTest.php b/Tests/Command/TraceableCommandTest.php
new file mode 100644
index 000000000..1bf709f8b
--- /dev/null
+++ b/Tests/Command/TraceableCommandTest.php
@@ -0,0 +1,67 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Tests\Command;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Command\TraceableCommand;
+use Symfony\Component\Console\Tester\CommandTester;
+use Symfony\Component\Console\Tests\Fixtures\LoopExampleCommand;
+use Symfony\Component\Stopwatch\Stopwatch;
+
+class TraceableCommandTest extends TestCase
+{
+ private Application $application;
+
+ protected function setUp(): void
+ {
+ $this->application = new Application();
+ $this->application->add(new LoopExampleCommand());
+ }
+
+ public function testRunIsOverriddenWithoutProfile()
+ {
+ $command = $this->application->find('app:loop:example');
+ $commandTester = new CommandTester($command);
+ $commandTester->execute([]);
+ $commandTester->assertCommandIsSuccessful();
+
+ $output = $commandTester->getDisplay();
+ $this->assertLoopOutputCorrectness($output);
+ }
+
+ public function testRunIsNotOverriddenWithProfile()
+ {
+ // Simulate the bug environment by wrapping
+ // our command in TraceableCommand, which is what Symfony does
+ // when you use the --profile option.
+ $command = new LoopExampleCommand();
+ $traceableCommand = new TraceableCommand($command, new Stopwatch());
+
+ $this->application->add($traceableCommand);
+
+ $commandTester = new CommandTester($traceableCommand);
+ $commandTester->execute([]);
+ $commandTester->assertCommandIsSuccessful();
+
+ $output = $commandTester->getDisplay();
+ $this->assertLoopOutputCorrectness($output);
+ }
+
+ public function assertLoopOutputCorrectness(string $output)
+ {
+ $completeChar = '\\' !== \DIRECTORY_SEPARATOR ? '▓' : '=';
+ self::assertMatchesRegularExpression('~3/3\s+\['.$completeChar.'+]\s+100%~u', $output);
+ self::assertStringContainsString('Loop finished.', $output);
+ self::assertEquals(3, substr_count($output, 'Hello world'));
+ }
+}
diff --git a/Tests/Completion/CompletionInputTest.php b/Tests/Completion/CompletionInputTest.php
index 5b6a8e42d..df0d081fd 100644
--- a/Tests/Completion/CompletionInputTest.php
+++ b/Tests/Completion/CompletionInputTest.php
@@ -132,4 +132,19 @@ public static function provideFromStringData()
yield ['bin/console cache:clear "multi word string"', ['bin/console', 'cache:clear', '"multi word string"']];
yield ['bin/console cache:clear \'multi word string\'', ['bin/console', 'cache:clear', '\'multi word string\'']];
}
+
+ public function testToString()
+ {
+ $input = CompletionInput::fromTokens(['foo', 'bar', 'baz'], 0);
+ $this->assertSame('foo| bar baz', (string) $input);
+
+ $input = CompletionInput::fromTokens(['foo', 'bar', 'baz'], 1);
+ $this->assertSame('foo bar| baz', (string) $input);
+
+ $input = CompletionInput::fromTokens(['foo', 'bar', 'baz'], 2);
+ $this->assertSame('foo bar baz|', (string) $input);
+
+ $input = CompletionInput::fromTokens(['foo', 'bar', 'baz'], 11);
+ $this->assertSame('foo bar baz |', (string) $input);
+ }
}
diff --git a/Tests/Completion/Output/FishCompletionOutputTest.php b/Tests/Completion/Output/FishCompletionOutputTest.php
index 2e615d040..93456e138 100644
--- a/Tests/Completion/Output/FishCompletionOutputTest.php
+++ b/Tests/Completion/Output/FishCompletionOutputTest.php
@@ -23,11 +23,11 @@ public function getCompletionOutput(): CompletionOutputInterface
public function getExpectedOptionsOutput(): string
{
- return "--option1\n--negatable\n--no-negatable";
+ return "--option1\tFirst Option\n--negatable\tCan be negative\n--no-negatable\tCan be negative";
}
public function getExpectedValuesOutput(): string
{
- return "Green\nRed\nYellow";
+ return "Green\tBeans are green\nRed\tRose are red\nYellow\tCanaries are yellow";
}
}
diff --git a/Tests/ConsoleEventsTest.php b/Tests/ConsoleEventsTest.php
index 93c228aa4..408f8c0d3 100644
--- a/Tests/ConsoleEventsTest.php
+++ b/Tests/ConsoleEventsTest.php
@@ -35,12 +35,11 @@ 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
- 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);
}
}
diff --git a/Tests/DependencyInjection/AddConsoleCommandPassTest.php b/Tests/DependencyInjection/AddConsoleCommandPassTest.php
index 639e5091e..9ac660100 100644
--- a/Tests/DependencyInjection/AddConsoleCommandPassTest.php
+++ b/Tests/DependencyInjection/AddConsoleCommandPassTest.php
@@ -15,6 +15,7 @@
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\LazyCommand;
+use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
@@ -176,6 +177,7 @@ public function testEscapesDefaultFromPhp()
$this->assertSame('%cmd%', $command->getName());
$this->assertSame(['%cmdalias%'], $command->getAliases());
$this->assertSame('Creates a 80% discount', $command->getDescription());
+ $this->assertSame('The %command.name% help content.', $command->getHelp());
}
public function testProcessThrowAnExceptionIfTheServiceIsAbstract()
@@ -206,7 +208,7 @@ public function testProcessThrowAnExceptionIfTheServiceIsNotASubclassOfCommand()
$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".');
+ $this->expectExceptionMessage('The service "my-command" tagged "console.command" must either be a subclass of "Symfony\Component\Console\Command\Command" or have an "__invoke()" method');
$container->compile();
}
@@ -303,6 +305,48 @@ public function testProcessOnChildDefinitionWithoutClass()
$container->compile();
}
+
+ public function testProcessInvokableCommand()
+ {
+ $container = new ContainerBuilder();
+ $container->addCompilerPass(new AddConsoleCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
+
+ $definition = new Definition(InvokableCommand::class);
+ $definition->addTag('console.command', [
+ 'command' => 'invokable',
+ 'description' => 'The command description',
+ 'help' => 'The %command.name% command help content.',
+ ]);
+ $container->setDefinition('invokable_command', $definition);
+
+ $container->compile();
+ $command = $container->get('console.command_loader')->get('invokable');
+
+ self::assertTrue($container->has('invokable_command.command'));
+ self::assertSame('The command description', $command->getDescription());
+ self::assertSame('The %command.name% command help content.', $command->getHelp());
+ }
+
+ public function testProcessInvokableSignalableCommand()
+ {
+ $container = new ContainerBuilder();
+ $container->addCompilerPass(new AddConsoleCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
+
+ $definition = new Definition(InvokableSignalableCommand::class);
+ $definition->addTag('console.command', [
+ 'command' => 'invokable-signalable',
+ 'description' => 'The command description',
+ 'help' => 'The %command.name% command help content.',
+ ]);
+ $container->setDefinition('invokable_signalable_command', $definition);
+
+ $container->compile();
+ $command = $container->get('console.command_loader')->get('invokable-signalable');
+
+ self::assertTrue($container->has('invokable_signalable_command.command'));
+ self::assertSame('The command description', $command->getDescription());
+ self::assertSame('The %command.name% command help content.', $command->getHelp());
+ }
}
class MyCommand extends Command
@@ -314,7 +358,7 @@ class NamedCommand extends Command
{
}
-#[AsCommand(name: '%cmd%|%cmdalias%', description: 'Creates a 80% discount')]
+#[AsCommand(name: '%cmd%|%cmdalias%', description: 'Creates a 80% discount', help: 'The %command.name% help content.')]
class EscapedDefaultsFromPhpCommand extends Command
{
}
@@ -331,3 +375,29 @@ public function __construct()
parent::__construct();
}
}
+
+#[AsCommand(name: 'invokable', description: 'Just testing', help: 'The %command.name% help content.')]
+class InvokableCommand
+{
+ public function __invoke(): void
+ {
+ }
+}
+
+#[AsCommand(name: 'invokable-signalable', description: 'Just testing', help: 'The %command.name% help content.')]
+class InvokableSignalableCommand implements SignalableCommandInterface
+{
+ public function __invoke(): void
+ {
+ }
+
+ public function getSubscribedSignals(): array
+ {
+ return [];
+ }
+
+ public function handleSignal(int $signal, false|int $previousExitCode = 0): int|false
+ {
+ return false;
+ }
+}
diff --git a/Tests/Descriptor/AbstractDescriptorTestCase.php b/Tests/Descriptor/AbstractDescriptorTestCase.php
index 1f813048d..93658f4be 100644
--- a/Tests/Descriptor/AbstractDescriptorTestCase.php
+++ b/Tests/Descriptor/AbstractDescriptorTestCase.php
@@ -87,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/JsonDescriptorTest.php b/Tests/Descriptor/JsonDescriptorTest.php
index 399bd8f23..914ed3597 100644
--- a/Tests/Descriptor/JsonDescriptorTest.php
+++ b/Tests/Descriptor/JsonDescriptorTest.php
@@ -36,10 +36,9 @@ private function normalizeOutputRecursively($output)
return array_map($this->normalizeOutputRecursively(...), $output);
}
- if (null === $output) {
- return null;
- }
-
- return parent::normalizeOutput($output);
+ return match ($output) {
+ null, true, false => $output,
+ default => parent::normalizeOutput($output),
+ };
}
}
diff --git a/Tests/Fixtures/AbstractLoopCommand.php b/Tests/Fixtures/AbstractLoopCommand.php
new file mode 100644
index 000000000..c3715067e
--- /dev/null
+++ b/Tests/Fixtures/AbstractLoopCommand.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Tests\Fixtures;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+abstract class AbstractLoopCommand extends Command
+{
+ public function run(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+ $contexts = [1, 2, 3];
+ $io->progressStart(count($contexts));
+ $code = self::SUCCESS;
+
+ foreach ($contexts as $ignored) {
+ $io->progressAdvance();
+ try {
+ parent::run($input, $output);
+ } catch (\Throwable) {
+ $code = self::FAILURE;
+ }
+ }
+ $io->progressFinish();
+ $output->writeln("\nLoop finished.");
+
+ return $code;
+ }
+}
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/FooLock4InvokableCommand.php b/Tests/Fixtures/FooLock4InvokableCommand.php
new file mode 100644
index 000000000..7309234fa
--- /dev/null
+++ b/Tests/Fixtures/FooLock4InvokableCommand.php
@@ -0,0 +1,22 @@
+lock()) {
+ return Command::FAILURE;
+ }
+
+ $this->release();
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/Tests/Fixtures/LoopExampleCommand.php b/Tests/Fixtures/LoopExampleCommand.php
new file mode 100644
index 000000000..d9eeb4db9
--- /dev/null
+++ b/Tests/Fixtures/LoopExampleCommand.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Tests\Fixtures;
+
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand('app:loop:example')]
+class LoopExampleCommand extends AbstractLoopCommand
+{
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $output->writeln(' Hello world');
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/Tests/Fixtures/MockableAppliationWithTerminalWidth.php b/Tests/Fixtures/MockableAppliationWithTerminalWidth.php
new file mode 100644
index 000000000..7f094ff3c
--- /dev/null
+++ b/Tests/Fixtures/MockableAppliationWithTerminalWidth.php
@@ -0,0 +1,22 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Tests\Fixtures;
+
+use Symfony\Component\Console\Application;
+
+class MockableAppliationWithTerminalWidth extends Application
+{
+ public function getTerminalWidth(): int
+ {
+ return 0;
+ }
+}
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_0.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_0.php
index 8fe7c0771..86095576c 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_0.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_0.php
@@ -5,7 +5,9 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure has single blank line at start when using block element
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->caution('Lorem ipsum dolor sit amet');
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_1.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_1.php
index e5c700d60..c72a3b390 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_1.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_1.php
@@ -5,9 +5,11 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure has single blank line between titles and blocks
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->title('Title');
$output->warning('Lorem ipsum dolor sit amet');
$output->title('Title');
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_10.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_10.php
index 3111873dd..c9bc1e30a 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_10.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_10.php
@@ -5,7 +5,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure that all lines are aligned to the begin of the first line in a very long line block
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->block(
'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',
@@ -14,4 +14,6 @@
'X ',
true
);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_11.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_11.php
index 3ed897def..838b66707 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_11.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_11.php
@@ -5,8 +5,10 @@
use Symfony\Component\Console\Style\SymfonyStyle;
// ensure long words are properly wrapped in blocks
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$word = 'Lopadotemachoselachogaleokranioleipsanodrimhypotrimmatosilphioparaomelitokatakechymenokichlepikossyphophattoperisteralektryonoptekephalliokigklopeleiolagoiosiraiobaphetraganopterygon';
$sfStyle = new SymfonyStyle($input, $output);
$sfStyle->block($word, 'CUSTOM', 'fg=white;bg=blue', ' § ', false);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_12.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_12.php
index 8c458ae76..24d64df8d 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_12.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_12.php
@@ -5,9 +5,11 @@
use Symfony\Component\Console\Style\SymfonyStyle;
// ensure that all lines are aligned to the begin of the first one and start with '//' in a very long line comment
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$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'
);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_13.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_13.php
index 9bcc68f69..4d0799770 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_13.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_13.php
@@ -5,10 +5,12 @@
use Symfony\Component\Console\Style\SymfonyStyle;
// ensure that nested tags have no effect on the color of the '//' prefix
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output->setDecorated(true);
$output = new SymfonyStyle($input, $output);
$output->comment(
'Á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'
);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_14.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_14.php
index a893a48bf..b079e4c5d 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_14.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_14.php
@@ -5,7 +5,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
// ensure that block() behaves properly with a prefix and without type
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->block(
'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',
@@ -14,4 +14,6 @@
'$ ',
true
);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_15.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_15.php
index 68402cd40..664a1938b 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_15.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_15.php
@@ -5,10 +5,12 @@
use Symfony\Component\Console\Style\SymfonyStyle;
// ensure that block() behaves properly with a type and without prefix
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->block(
'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',
'TEST'
);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_16.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_16.php
index 66e817963..2b7bba059 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_16.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_16.php
@@ -5,11 +5,13 @@
use Symfony\Component\Console\Style\SymfonyStyle;
// ensure that block() output is properly formatted (even padding lines)
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output->setDecorated(true);
$output = new SymfonyStyle($input, $output);
$output->success(
'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',
'TEST'
);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_17.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_17.php
index 311e6b392..399a5a06f 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_17.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_17.php
@@ -5,9 +5,11 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure symfony style helper methods handle trailing backslashes properly when decorating user texts
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->title('Title ending with \\');
$output->section('Section ending with \\');
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_18.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_18.php
index d4afa45cf..383615a34 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_18.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_18.php
@@ -5,7 +5,7 @@
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->definitionList(
@@ -15,4 +15,6 @@
new TableSeparator(),
['foo2' => 'bar2']
);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_19.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_19.php
index e44b18b76..3e57f66ca 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_19.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_19.php
@@ -1,12 +1,13 @@
horizontalTable(['a', 'b', 'c', 'd'], [[1, 2, 3], [4, 5], [7, 8, 9]]);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_2.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_2.php
index a16ad505d..5bba34f36 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_2.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_2.php
@@ -5,7 +5,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure has single blank line between blocks
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->warning('Warning');
$output->caution('Caution');
@@ -14,4 +14,6 @@
$output->note('Note');
$output->info('Info');
$output->block('Custom block', 'CUSTOM', 'fg=white;bg=green', 'X ', true);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_20.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_20.php
index 6b47969ee..3bdd5d5cf 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_20.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_20.php
@@ -5,9 +5,11 @@
use Symfony\Component\Console\Style\SymfonyStyle;
// Ensure that closing tag is applied once
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output->setDecorated(true);
$output = new SymfonyStyle($input, $output);
$output->write('do you want something>');
$output->writeln('?>');
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_21.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_21.php
index 8460e7ece..3faf7c7a0 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_21.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_21.php
@@ -5,9 +5,11 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure texts with emojis don't make longer lines than expected
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->success('Lorem ipsum dolor sit amet');
$output->success('Lorem ipsum dolor sit amet with one emoji 🎉');
$output->success('Lorem ipsum dolor sit amet with so many of them 👩🌾👩🌾👩🌾👩🌾👩🌾');
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_22.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_22.php
index 1070394a8..3ec61081b 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_22.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_22.php
@@ -5,7 +5,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
// ensure that nested tags have no effect on the color of the '//' prefix
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output->setDecorated(true);
$output = new SymfonyStyle($input, $output);
$output->block(
@@ -16,4 +16,6 @@
false,
false
);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_23.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_23.php
index e6228fe0b..618de55ce 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_23.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_23.php
@@ -4,7 +4,9 @@
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->text('Hello');
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_3.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_3.php
index 99253a6c0..b6a3cd27c 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_3.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_3.php
@@ -5,8 +5,10 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure has single blank line between two titles
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->title('First title');
$output->title('Second title');
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_4.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_4.php
index b2f3d9954..d196735c1 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_4.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_4.php
@@ -5,7 +5,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure has single blank line after any text and a title
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->write('Lorem ipsum dolor sit amet');
@@ -31,4 +31,6 @@
$output->writeln('Lorem ipsum dolor sit amet');
$output->newLine(2); //Should append an extra blank line
$output->title('Fifth title');
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_4_with_iterators.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_4_with_iterators.php
index 3b215c7f2..24de2cab3 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_4_with_iterators.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_4_with_iterators.php
@@ -5,7 +5,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure has single blank line after any text and a title
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->write('Lorem ipsum dolor sit amet');
@@ -31,4 +31,6 @@
$output->writeln('Lorem ipsum dolor sit amet');
$output->newLine(2); //Should append an extra blank line
$output->title('Fifth title');
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_5.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_5.php
index 6fba5737f..6fab68233 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_5.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_5.php
@@ -5,7 +5,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure has proper line ending before outputting a text block like with SymfonyStyle::listing() or SymfonyStyle::text()
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->writeln('Lorem ipsum dolor sit amet');
@@ -34,4 +34,6 @@
'Lorem ipsum dolor sit amet',
'consectetur adipiscing elit',
]);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_6.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_6.php
index 3278f6ea0..cef96d5d9 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_6.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_6.php
@@ -5,7 +5,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure has proper blank line after text block when using a block like with SymfonyStyle::success
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->listing([
@@ -13,4 +13,6 @@
'consectetur adipiscing elit',
]);
$output->success('Lorem ipsum dolor sit amet');
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_7.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_7.php
index 037c6ab6b..f4f673c17 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_7.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_7.php
@@ -5,11 +5,13 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure questions do not output anything when input is non-interactive
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->title('Title');
$output->askHidden('Hidden question');
$output->choice('Choice question with default', ['choice1', 'choice2'], 'choice1');
$output->confirm('Confirmation with yes default', true);
$output->text('Duis aute irure dolor in reprehenderit in voluptate velit esse');
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_8.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_8.php
index fe9d484d2..856665451 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_8.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_8.php
@@ -6,7 +6,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure formatting tables when using multiple headers with TableCell
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$headers = [
[new TableCell('Main table title', ['colspan' => 3])],
['ISBN', 'Title', 'Author'],
@@ -23,4 +23,6 @@
$output = new SymfonyStyle($input, $output);
$output->table($headers, $rows);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/command_9.php b/Tests/Fixtures/Style/SymfonyStyle/command/command_9.php
index 73af4ae1e..77dd8d087 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/command_9.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/command_9.php
@@ -5,7 +5,9 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure that all lines are aligned to the begin of the first line in a multi-line block
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$output->block(['Custom block', 'Second custom block line'], 'CUSTOM', 'fg=white;bg=green', 'X ', true);
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/command/interactive_command_1.php b/Tests/Fixtures/Style/SymfonyStyle/command/interactive_command_1.php
index 3c9c74405..7855f9dcd 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/command/interactive_command_1.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/command/interactive_command_1.php
@@ -5,7 +5,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
//Ensure that questions have the expected outputs
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$output = new SymfonyStyle($input, $output);
$stream = fopen('php://memory', 'r+', false);
@@ -16,4 +16,6 @@
$output->ask('What\'s your name?');
$output->ask('How are you?');
$output->ask('Where do you come from?');
+
+ return 0;
};
diff --git a/Tests/Fixtures/Style/SymfonyStyle/progress/command_progress_iterate.php b/Tests/Fixtures/Style/SymfonyStyle/progress/command_progress_iterate.php
index 6487bc3b1..3744c9b22 100644
--- a/Tests/Fixtures/Style/SymfonyStyle/progress/command_progress_iterate.php
+++ b/Tests/Fixtures/Style/SymfonyStyle/progress/command_progress_iterate.php
@@ -5,7 +5,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
// progressIterate
-return function (InputInterface $input, OutputInterface $output) {
+return function (InputInterface $input, OutputInterface $output): int {
$style = new SymfonyStyle($input, $output);
foreach ($style->progressIterate(\range(1, 10)) as $step) {
@@ -13,4 +13,6 @@
}
$style->writeln('end of progressbar');
+
+ return 0;
};
diff --git a/Tests/Fixtures/application_1.json b/Tests/Fixtures/application_1.json
index bd0bd94c7..1477659ad 100644
--- a/Tests/Fixtures/application_1.json
+++ b/Tests/Fixtures/application_1.json
@@ -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": {
@@ -150,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": {
@@ -262,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": {
@@ -365,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 bb722c077..79d9b27aa 100644
--- a/Tests/Fixtures/application_1.md
+++ b/Tests/Fixtures/application_1.md
@@ -48,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
@@ -58,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
@@ -159,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
@@ -169,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
@@ -286,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
@@ -296,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.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 d109e055f..d726cee35 100644
--- a/Tests/Fixtures/application_1.xml
+++ b/Tests/Fixtures/application_1.xml
@@ -32,9 +32,12 @@
-
+
@@ -71,9 +74,12 @@
-
+
@@ -126,9 +132,12 @@
-
+
@@ -188,9 +197,12 @@
-
+
diff --git a/Tests/Fixtures/application_2.json b/Tests/Fixtures/application_2.json
index b3eb10bda..c0e66444e 100644
--- a/Tests/Fixtures/application_2.json
+++ b/Tests/Fixtures/application_2.json
@@ -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": {
@@ -85,7 +94,7 @@
"is_value_required": false,
"is_multiple": false,
"description": "Do not ask any interactive question",
- "default": false
+ "default": null
},
"shell": {
"name": "--shell",
@@ -154,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": {
@@ -206,7 +224,7 @@
"is_value_required": false,
"is_multiple": false,
"description": "Do not ask any interactive question",
- "default": false
+ "default": null
},
"debug": {
"name": "--debug",
@@ -266,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": {
@@ -318,7 +345,7 @@
"is_value_required": false,
"is_multiple": false,
"description": "Do not ask any interactive question",
- "default": false
+ "default": null
}
}
}
@@ -369,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": {
@@ -421,7 +457,7 @@
"is_value_required": false,
"is_multiple": false,
"description": "Do not ask any interactive question",
- "default": false
+ "default": null
},
"short": {
"name": "--short",
@@ -457,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": {
@@ -509,7 +554,7 @@
"is_value_required": false,
"is_multiple": false,
"description": "Do not ask any interactive question",
- "default": false
+ "default": null
}
}
}
@@ -553,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": {
@@ -605,7 +659,7 @@
"is_value_required": false,
"is_multiple": false,
"description": "Do not ask any interactive question",
- "default": false
+ "default": null
}
}
}
@@ -630,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": {
@@ -682,7 +745,7 @@
"is_value_required": false,
"is_multiple": false,
"description": "Do not ask any interactive question",
- "default": false
+ "default": null
}
}
}
@@ -709,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": {
@@ -761,7 +833,7 @@
"is_value_required": false,
"is_multiple": false,
"description": "Do not ask any interactive question",
- "default": false
+ "default": null
}
}
}
diff --git a/Tests/Fixtures/application_2.md b/Tests/Fixtures/application_2.md
index d4802c747..37e6c28fc 100644
--- a/Tests/Fixtures/application_2.md
+++ b/Tests/Fixtures/application_2.md
@@ -61,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
@@ -71,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
@@ -172,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
@@ -182,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
@@ -299,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
@@ -309,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
@@ -374,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
@@ -384,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
@@ -465,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
@@ -475,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
@@ -537,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
@@ -547,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.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.0Options:-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 6ee45c1fa..dd4b1800a 100644
--- a/Tests/Fixtures/application_2.xml
+++ b/Tests/Fixtures/application_2.xml
@@ -32,9 +32,12 @@
-
+
@@ -71,9 +74,12 @@
-
+
@@ -126,9 +132,12 @@
-
+
@@ -188,9 +197,12 @@
-
+
@@ -221,9 +233,12 @@
-
+
@@ -262,9 +277,12 @@
-
+
@@ -293,9 +311,12 @@
-
+
@@ -326,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.0Options:-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 e7bc69c71..5e31b7ef4 100644
--- a/Tests/Fixtures/application_mbstring.md
+++ b/Tests/Fixtures/application_mbstring.md
@@ -52,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
@@ -62,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
@@ -163,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
@@ -173,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
@@ -290,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
@@ -300,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
@@ -381,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
@@ -391,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.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 12cf744ea..cc1bae6ac 100644
--- a/Tests/Fixtures/application_signalable.php
+++ b/Tests/Fixtures/application_signalable.php
@@ -1,6 +1,5 @@
setCode(function(InputInterface $input, OutputInterface $output) {
+ ->setCode(function(InputInterface $input, OutputInterface $output): int {
$this->getHelper('question')
->ask($input, $output, new ChoiceQuestion('😊', ['y']));
diff --git a/Tests/Formatter/OutputFormatterTest.php b/Tests/Formatter/OutputFormatterTest.php
index 610522a7e..489108bd5 100644
--- a/Tests/Formatter/OutputFormatterTest.php
+++ b/Tests/Formatter/OutputFormatterTest.php
@@ -162,7 +162,7 @@ public function testInlineStyle()
/**
* @dataProvider provideInlineStyleOptionsCases
*/
- public function testInlineStyleOptions(string $tag, string $expected = null, string $input = null, bool $truecolor = false)
+ public function testInlineStyleOptions(string $tag, ?string $expected = null, ?string $input = null, bool $truecolor = false)
{
if ($truecolor && 'truecolor' !== getenv('COLORTERM')) {
$this->markTestSkipped('The terminal does not support true colors.');
@@ -177,7 +177,7 @@ public function testInlineStyleOptions(string $tag, string $expected = null, str
$expected = $tag.$input.''.$styleString.'>';
$this->assertSame($expected, $formatter->format($expected));
} else {
- /* @var OutputFormatterStyle $result */
+ /** @var OutputFormatterStyle $result */
$this->assertInstanceOf(OutputFormatterStyle::class, $result);
$this->assertSame($expected, $formatter->format($tag.$input.'>'));
$this->assertSame($expected, $formatter->format($tag.$input.''.$styleString.'>'));
@@ -199,7 +199,7 @@ public static function provideInlineStyleOptionsCases()
];
}
- public function provideInlineStyleTagsWithUnknownOptions()
+ public static function provideInlineStyleTagsWithUnknownOptions()
{
return [
['', 'abc'],
@@ -365,6 +365,15 @@ public function testFormatAndWrap()
$this->assertSame("Lore\nm \e[37;41mip\e[39;49m\n\e[37;41msum\e[39;49m \ndolo\nr \e[32msi\e[39m\n\e[32mt\e[39m am\net", $formatter->formatAndWrap('Lorem ipsum dolor sit amet', 4));
$this->assertSame("Lorem \e[37;41mip\e[39;49m\n\e[37;41msum\e[39;49m dolo\nr \e[32msit\e[39m am\net", $formatter->formatAndWrap('Lorem ipsum dolor sit amet', 8));
$this->assertSame("Lorem \e[37;41mipsum\e[39;49m dolor \e[32m\e[39m\n\e[32msit\e[39m, \e[37;41mamet\e[39;49m et \e[32mlauda\e[39m\n\e[32mntium\e[39m architecto", $formatter->formatAndWrap('Lorem ipsum dolor sit, amet et laudantium architecto', 18));
+ $this->assertSame("\e[37;41mnon-empty-array\e[39;49m\e[37;41m\e[39;49m given.\n🪪 argument.type", $formatter->formatAndWrap("non-empty-array given.\n🪪 argument.type", 38));
+ $this->assertSame("Usuário {{user_name}} não é válid\no.", $formatter->formatAndWrap('Usuário {{user_name}} não é válido.', 50));
+ $this->assertSame("foo\e[37;41mb\e[39;49m\n\e[37;41mar\e[39;49mbaz", $formatter->formatAndWrap("foob\narbaz", 7));
+ $this->assertSame("foo\e[37;41mbar\e[39;49mbaz\nnewline", $formatter->formatAndWrap("foobarbaz\nnewline", 11));
+ $this->assertSame("foobarbaz\n\e[37;41mnewline\e[39;49m", $formatter->formatAndWrap("foobarbaz\nnewline", 11));
+ $this->assertSame("foobar\e[37;41mbaz\e[39;49m\n\e[37;41mnewline\e[39;49m", $formatter->formatAndWrap("foobarbaz\nnewline", 11));
+ $this->assertSame("foobar\e[37;41mbazne\e[39;49m\n\e[37;41mwline\e[39;49m", $formatter->formatAndWrap("foobarbazne\nwline", 11));
+ $this->assertSame("foobar\e[37;41mbazne\e[39;49m\n\e[37;41mw\e[39;49m\n\e[37;41mline\e[39;49m", $formatter->formatAndWrap("foobarbaznew\nline", 11));
+ $this->assertSame("\e[37;41m👩🌾\e[39;49m", $formatter->formatAndWrap('👩🌾', 1));
$formatter = new OutputFormatter();
@@ -376,6 +385,15 @@ public function testFormatAndWrap()
$this->assertSame("Â rèälly\nlöng tîtlè\nthät cöüld\nnèêd\nmúltîplê\nlínès", $formatter->formatAndWrap('Â rèälly löng tîtlè thät cöüld nèêd múltîplê línès', 10));
$this->assertSame("Â rèälly\nlöng tîtlè\nthät cöüld\nnèêd\nmúltîplê\n línès", $formatter->formatAndWrap("Â rèälly löng tîtlè thät cöüld nèêd múltîplê\n línès", 10));
$this->assertSame('', $formatter->formatAndWrap(null, 5));
+ $this->assertSame("non-empty-array given.\n🪪 argument.type", $formatter->formatAndWrap("non-empty-array given.\n🪪 argument.type", 38));
+ $this->assertSame("Usuário {{user_name}} não é válid\no.", $formatter->formatAndWrap('Usuário {{user_name}} não é válido.', 50));
+ $this->assertSame("foob\narbaz", $formatter->formatAndWrap("foob\narbaz", 7));
+ $this->assertSame("foobarbaz\nnewline", $formatter->formatAndWrap("foobarbaz\nnewline", 11));
+ $this->assertSame("foobarbaz\nnewline", $formatter->formatAndWrap("foobarbaz\nnewline", 11));
+ $this->assertSame("foobarbaz\nnewline", $formatter->formatAndWrap("foobarbaz\nnewline", 11));
+ $this->assertSame("foobarbazne\nwline", $formatter->formatAndWrap("foobarbazne\nwline", 11));
+ $this->assertSame("foobarbazne\nw\nline", $formatter->formatAndWrap("foobarbaznew\nline", 11));
+ $this->assertSame('👩🌾', $formatter->formatAndWrap('👩🌾', 1));
}
}
diff --git a/Tests/Helper/HelperSetTest.php b/Tests/Helper/HelperSetTest.php
index 9fbb9afca..389ee0ed3 100644
--- a/Tests/Helper/HelperSetTest.php
+++ b/Tests/Helper/HelperSetTest.php
@@ -87,7 +87,7 @@ public function testIteration()
}
}
- private function getGenericMockHelper($name, HelperSet $helperset = null)
+ private function getGenericMockHelper($name, ?HelperSet $helperset = null)
{
$mock_helper = $this->createMock(HelperInterface::class);
$mock_helper->expects($this->any())
diff --git a/Tests/Helper/HelperTest.php b/Tests/Helper/HelperTest.php
index 0a0c2fa48..009864454 100644
--- a/Tests/Helper/HelperTest.php
+++ b/Tests/Helper/HelperTest.php
@@ -20,31 +20,34 @@ class HelperTest extends TestCase
public static function formatTimeProvider()
{
return [
- [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],
+ [0, '< 1 ms', 1],
+ [0.0004, '< 1 ms', 1],
+ [0.95, '950 ms', 1],
+ [1, '1 s', 1],
+ [2, '2 s', 2],
+ [59, '59 s', 1],
+ [59.21, '59 s', 1],
+ [59.21, '59 s, 210 ms', 5],
[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],
+ [61, '1 min, 1 s', 2],
+ [119, '1 min, 59 s', 2],
+ [120, '2 min', 2],
+ [121, '2 min, 1 s', 2],
+ [3599, '59 min, 59 s', 2],
+ [3600, '1 h', 2],
+ [7199, '1 h, 59 min', 2],
+ [7200, '2 h', 2],
+ [7201, '2 h', 2],
+ [86399, '23 h, 59 min', 2],
+ [86399, '23 h, 59 min, 59 s', 3],
+ [86400, '1 d', 2],
+ [86401, '1 d', 2],
+ [172799, '1 d, 23 h', 2],
+ [172799, '1 d, 23 h, 59 min, 59 s', 4],
+ [172799.123, '1 d, 23 h, 59 min, 59 s, 123 ms', 5],
+ [172800, '2 d', 2],
+ [172801, '2 d', 2],
+ [172801, '2 d, 1 s', 4],
];
}
diff --git a/Tests/Helper/ProgressBarTest.php b/Tests/Helper/ProgressBarTest.php
index 4dff078ae..c0278cc33 100644
--- a/Tests/Helper/ProgressBarTest.php
+++ b/Tests/Helper/ProgressBarTest.php
@@ -110,6 +110,16 @@ public function testRegularTimeEstimation()
);
}
+ 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);
@@ -406,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
@@ -948,7 +1033,7 @@ public function testAnsiColorsAndEmojis()
$this->assertEquals(
" \033[44;37m Starting the demo... fingers crossed \033[0m\n".
' 0/15 '.$progress.str_repeat($empty, 26)." 0%\n".
- " \xf0\x9f\x8f\x81 < 1 sec \033[44;37m 0 B \033[0m",
+ " \xf0\x9f\x8f\x81 < 1 ms \033[44;37m 0 B \033[0m",
stream_get_contents($output->getStream())
);
ftruncate($output->getStream(), 0);
@@ -962,7 +1047,7 @@ public function testAnsiColorsAndEmojis()
$this->generateOutput(
" \033[44;37m Looks good to me... \033[0m\n".
' 4/15 '.str_repeat($done, 7).$progress.str_repeat($empty, 19)." 26%\n".
- " \xf0\x9f\x8f\x81 < 1 sec \033[41;37m 97 KiB \033[0m"
+ " \xf0\x9f\x8f\x81 < 1 ms \033[41;37m 97 KiB \033[0m"
),
stream_get_contents($output->getStream())
);
@@ -977,7 +1062,7 @@ public function testAnsiColorsAndEmojis()
$this->generateOutput(
" \033[44;37m Thanks, bye \033[0m\n".
' 15/15 '.str_repeat($done, 28)." 100%\n".
- " \xf0\x9f\x8f\x81 < 1 sec \033[41;37m 195 KiB \033[0m"
+ " \xf0\x9f\x8f\x81 < 1 ms \033[41;37m 195 KiB \033[0m"
),
stream_get_contents($output->getStream())
);
@@ -1012,7 +1097,7 @@ public function testSetFormatWithTimes()
$bar->start();
rewind($output->getStream());
$this->assertEquals(
- ' 0/15 [>---------------------------] 0% < 1 sec/< 1 sec/< 1 sec',
+ ' 0/15 [>---------------------------] 0% < 1 ms/< 1 ms/< 1 ms',
stream_get_contents($output->getStream())
);
}
@@ -1092,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 ms/< 1 ms',
+ stream_get_contents($output->getStream())
+ );
+ }
+
protected function getOutputStream($decorated = true, $verbosity = StreamOutput::VERBOSITY_NORMAL)
{
return new StreamOutput(fopen('php://memory', 'r+', false), $verbosity, $decorated);
@@ -1263,8 +1362,15 @@ public function testMultiLineFormatIsFullyCorrectlyWithManuallyCleanup()
'Foo!'.\PHP_EOL.
$this->generateOutput('[--->------------------------]').
"\nProcessing \"foobar\"...".
- $this->generateOutput("[----->----------------------]\nProcessing \"foobar\"..."),
+ $this->generateOutput("[============================]\nProcessing \"foobar\"..."),
stream_get_contents($output->getStream())
);
}
+
+ public function testGetNotSetMessage()
+ {
+ $progressBar = new ProgressBar($this->getOutputStream());
+
+ $this->assertNull($progressBar->getMessage());
+ }
}
diff --git a/Tests/Helper/ProgressIndicatorTest.php b/Tests/Helper/ProgressIndicatorTest.php
index 7f7dbc0a0..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);
@@ -179,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 8c5fe8a20..76e40cef0 100644
--- a/Tests/Helper/QuestionHelperTest.php
+++ b/Tests/Helper/QuestionHelperTest.php
@@ -519,7 +519,7 @@ public function testAskMultilineResponseWithWithCursorInMiddleOfSeekableInputStr
$question->setMultiline(true);
$this->assertSame("some\ninput", $dialog->ask($this->createStreamableInputInterfaceMock($response), $this->createOutputInterface(), $question));
- $this->assertSame(8, ftell($response));
+ $this->assertSame(18, ftell($response));
}
/**
@@ -777,7 +777,7 @@ public function testQuestionValidatorRepeatsThePrompt()
$application = new Application();
$application->setAutoExit(false);
$application->register('question')
- ->setCode(function ($input, $output) use (&$tries) {
+ ->setCode(function (InputInterface $input, OutputInterface $output) use (&$tries): int {
$question = new Question('This is a promptable question');
$question->setValidator(function ($value) use (&$tries) {
++$tries;
@@ -908,6 +908,10 @@ public function testTraversableMultiselectAutocomplete()
public function testAutocompleteMoveCursorBackwards()
{
+ if (!Terminal::hasSttyAvailable()) {
+ $this->markTestSkipped('`stty` is required to test autocomplete functionality');
+ }
+
// F
$inputStream = $this->getInputStream("F\t\177\177\177");
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/TableTest.php b/Tests/Helper/TableTest.php
index 728ea847f..eb85364da 100644
--- a/Tests/Helper/TableTest.php
+++ b/Tests/Helper/TableTest.php
@@ -102,7 +102,7 @@ public static function renderProvider()
['ISBN', 'Title', 'Author'],
$books,
'default',
-<<<'TABLE'
+ <<<'TABLE'
+---------------+--------------------------+------------------+
| ISBN | Title | Author |
+---------------+--------------------------+------------------+
@@ -112,7 +112,21 @@ public static function renderProvider()
| 80-902734-1-6 | And Then There Were None | Agatha Christie |
+---------------+--------------------------+------------------+
-TABLE
+TABLE,
+ ],
+ [
+ ['ISBN', 'Title', 'Author'],
+ $books,
+ 'markdown',
+ <<<'TABLE'
+| ISBN | Title | Author |
+|---------------|--------------------------|------------------|
+| 99921-58-10-7 | Divine Comedy | Dante Alighieri |
+| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
+| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
+| 80-902734-1-6 | And Then There Were None | Agatha Christie |
+
+TABLE,
],
[
['ISBN', 'Title', 'Author'],
@@ -157,7 +171,7 @@ public static function renderProvider()
│ 80-902734-1-6 │ And Then There Were None │ Agatha Christie │
└───────────────┴──────────────────────────┴──────────────────┘
-TABLE
+TABLE,
],
[
['ISBN', 'Title', 'Author'],
@@ -180,7 +194,7 @@ public static function renderProvider()
║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║
╚═══════════════╧══════════════════════════╧══════════════════╝
-TABLE
+TABLE,
],
[
['ISBN', 'Title'],
@@ -191,7 +205,7 @@ public static function renderProvider()
['80-902734-1-6', 'And Then There Were None', 'Agatha Christie'],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+---------------+--------------------------+------------------+
| ISBN | Title | |
+---------------+--------------------------+------------------+
@@ -201,7 +215,7 @@ public static function renderProvider()
| 80-902734-1-6 | And Then There Were None | Agatha Christie |
+---------------+--------------------------+------------------+
-TABLE
+TABLE,
],
[
[],
@@ -212,7 +226,7 @@ public static function renderProvider()
['80-902734-1-6', 'And Then There Were None', 'Agatha Christie'],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+---------------+--------------------------+------------------+
| 99921-58-10-7 | Divine Comedy | Dante Alighieri |
| 9971-5-0210-0 | | |
@@ -220,7 +234,7 @@ public static function renderProvider()
| 80-902734-1-6 | And Then There Were None | Agatha Christie |
+---------------+--------------------------+------------------+
-TABLE
+TABLE,
],
[
['ISBN', 'Title', 'Author'],
@@ -231,7 +245,7 @@ public static function renderProvider()
['960-425-059-0', 'The Lord of the Rings', "J. R. R.\nTolkien"],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+---------------+----------------------------+-----------------+
| ISBN | Title | Author |
+---------------+----------------------------+-----------------+
@@ -245,18 +259,18 @@ public static function renderProvider()
| | | Tolkien |
+---------------+----------------------------+-----------------+
-TABLE
+TABLE,
],
[
['ISBN', 'Title'],
[],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+------+-------+
| ISBN | Title |
+------+-------+
-TABLE
+TABLE,
],
[
[],
@@ -271,7 +285,7 @@ public static function renderProvider()
['9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens>'],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+---------------+----------------------+-----------------+
| ISBN | Title | Author |
+---------------+----------------------+-----------------+
@@ -279,7 +293,7 @@ public static function renderProvider()
| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
+---------------+----------------------+-----------------+
-TABLE
+TABLE,
],
'Cell text with tags not used for Output styling' => [
['ISBN', 'Title', 'Author'],
@@ -288,7 +302,7 @@ public static function renderProvider()
['9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens'],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+----------------------------------+----------------------+-----------------+
| ISBN | Title | Author |
+----------------------------------+----------------------+-----------------+
@@ -296,7 +310,7 @@ public static function renderProvider()
| 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
+----------------------------------+----------------------+-----------------+
-TABLE
+TABLE,
],
'Cell with colspan' => [
['ISBN', 'Title', 'Author'],
@@ -320,7 +334,7 @@ public static function renderProvider()
],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+-------------------------------+-------------------------------+-----------------------------+
| ISBN | Title | Author |
+-------------------------------+-------------------------------+-----------------------------+
@@ -336,7 +350,7 @@ public static function renderProvider()
| Cupìdĭtâte díctá âtquè pôrrò, tèmpórà exercitátìónèm mòdí ânìmí núllà nèmò vèl níhìl! |
+-------------------------------+-------------------------------+-----------------------------+
-TABLE
+TABLE,
],
'Cell after colspan contains new line break' => [
['Foo', 'Bar', 'Baz'],
@@ -347,7 +361,7 @@ public static function renderProvider()
],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+-----+-----+-----+
| Foo | Bar | Baz |
+-----+-----+-----+
@@ -355,7 +369,7 @@ public static function renderProvider()
| bar | qux |
+-----+-----+-----+
-TABLE
+TABLE,
],
'Cell after colspan contains multiple new lines' => [
['Foo', 'Bar', 'Baz'],
@@ -366,7 +380,7 @@ public static function renderProvider()
],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+-----+-----+------+
| Foo | Bar | Baz |
+-----+-----+------+
@@ -375,7 +389,7 @@ public static function renderProvider()
| | quux |
+-----+-----+------+
-TABLE
+TABLE,
],
'Cell with rowspan' => [
['ISBN', 'Title', 'Author'],
@@ -392,7 +406,7 @@ public static function renderProvider()
['80-902734-1-7', 'Test'],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+---------------+---------------+-----------------+
| ISBN | Title | Author |
+---------------+---------------+-----------------+
@@ -406,7 +420,7 @@ public static function renderProvider()
| | Were None | |
+---------------+---------------+-----------------+
-TABLE
+TABLE,
],
'Cell with rowspan and colspan' => [
['ISBN', 'Title', 'Author'],
@@ -425,7 +439,7 @@ public static function renderProvider()
['J. R. R'],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+------------------+---------+-----------------+
| ISBN | Title | Author |
+------------------+---------+-----------------+
@@ -437,7 +451,7 @@ public static function renderProvider()
| J. R. R | |
+------------------+---------+-----------------+
-TABLE
+TABLE,
],
'Cell with rowspan and colspan contains new line break' => [
['ISBN', 'Title', 'Author'],
@@ -460,7 +474,7 @@ public static function renderProvider()
],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+-----------------+-------+-----------------+
| ISBN | Title | Author |
+-----------------+-------+-----------------+
@@ -480,7 +494,7 @@ public static function renderProvider()
| 0-0 | |
+-----------------+-------+-----------------+
-TABLE
+TABLE,
],
'Cell with rowspan and colspan without using TableSeparator' => [
['ISBN', 'Title', 'Author'],
@@ -497,7 +511,7 @@ public static function renderProvider()
['Charles Dickens'],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+-----------------+-------+-----------------+
| ISBN | Title | Author |
+-----------------+-------+-----------------+
@@ -511,7 +525,7 @@ public static function renderProvider()
| | 0-0 |
+-----------------+-------+-----------------+
-TABLE
+TABLE,
],
'Cell with rowspan and colspan with separator inside a rowspan' => [
['ISBN', 'Author'],
@@ -524,7 +538,7 @@ public static function renderProvider()
['Charles Dickens'],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+---------------+-----------------+
| ISBN | Author |
+---------------+-----------------+
@@ -533,7 +547,7 @@ public static function renderProvider()
| | Charles Dickens |
+---------------+-----------------+
-TABLE
+TABLE,
],
'Multiple header lines' => [
[
@@ -542,14 +556,14 @@ public static function renderProvider()
],
[],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+------+-------+--------+
| Main title |
+------+-------+--------+
| ISBN | Title | Author |
+------+-------+--------+
-TABLE
+TABLE,
],
'Row with multiple cells' => [
[],
@@ -560,14 +574,14 @@ public static function renderProvider()
new TableCell('3', ['colspan' => 2]),
new TableCell('4', ['colspan' => 2]),
],
- ],
+ ],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+---+--+--+---+--+---+--+---+--+
| 1 | 2 | 3 | 4 |
+---+--+--+---+--+---+--+---+--+
-TABLE
+TABLE,
],
'Coslpan and table cells with comment style' => [
[
@@ -595,7 +609,7 @@ public static function renderProvider()
+-----------------+------------------+---------+
TABLE
- ,
+ ,
true,
],
'Row with formatted cells containing a newline' => [
@@ -607,7 +621,7 @@ public static function renderProvider()
new TableSeparator(),
[
'foo',
- new TableCell('Dont break'."\n".'here', ['rowspan' => 2]),
+ new TableCell('Dont break'."\n".'here', ['rowspan' => 2]),
],
[
'bar',
@@ -624,77 +638,77 @@ public static function renderProvider()
+-------+------------+
TABLE
- ,
+ ,
true,
],
'TabeCellStyle with align. Also with rowspan and colspan > 1' => [
- [
- new TableCell(
- 'ISBN',
- [
- 'style' => new TableCellStyle([
- 'align' => 'right',
- ]),
- ]
- ),
- 'Title',
- new TableCell(
- 'Author',
- [
- 'style' => new TableCellStyle([
- 'align' => 'center',
- ]),
- ]
- ),
- ],
- [
- [
- new TableCell(
- '978>',
- [
- 'style' => new TableCellStyle([
- 'align' => 'center',
- ]),
- ]
- ),
- 'De Monarchia',
- new TableCell(
- "Dante Alighieri \nspans multiple rows rows Dante Alighieri \nspans multiple rows rows",
- [
- 'rowspan' => 2,
- 'style' => new TableCellStyle([
- 'align' => 'center',
- ]),
- ]
- ),
- ],
- [
- '99921-58-10-7',
- 'Divine Comedy',
- ],
- new TableSeparator(),
- [
- new TableCell(
- 'test',
- [
- 'colspan' => 2,
- 'style' => new TableCellStyle([
- 'align' => 'center',
- ]),
- ]
- ),
- new TableCell(
- 'tttt',
- [
- 'style' => new TableCellStyle([
- 'align' => 'right',
- ]),
- ]
- ),
- ],
- ],
- 'default',
-<<<'TABLE'
+ [
+ new TableCell(
+ 'ISBN',
+ [
+ 'style' => new TableCellStyle([
+ 'align' => 'right',
+ ]),
+ ]
+ ),
+ 'Title',
+ new TableCell(
+ 'Author',
+ [
+ 'style' => new TableCellStyle([
+ 'align' => 'center',
+ ]),
+ ]
+ ),
+ ],
+ [
+ [
+ new TableCell(
+ '978>',
+ [
+ 'style' => new TableCellStyle([
+ 'align' => 'center',
+ ]),
+ ]
+ ),
+ 'De Monarchia',
+ new TableCell(
+ "Dante Alighieri \nspans multiple rows rows Dante Alighieri \nspans multiple rows rows",
+ [
+ 'rowspan' => 2,
+ 'style' => new TableCellStyle([
+ 'align' => 'center',
+ ]),
+ ]
+ ),
+ ],
+ [
+ '99921-58-10-7',
+ 'Divine Comedy',
+ ],
+ new TableSeparator(),
+ [
+ new TableCell(
+ 'test',
+ [
+ 'colspan' => 2,
+ 'style' => new TableCellStyle([
+ 'align' => 'center',
+ ]),
+ ]
+ ),
+ new TableCell(
+ 'tttt',
+ [
+ 'style' => new TableCellStyle([
+ 'align' => 'right',
+ ]),
+ ]
+ ),
+ ],
+ ],
+ 'default',
+ <<<'TABLE'
+---------------+---------------+-------------------------------------------+
| ISBN | Title | Author |
+---------------+---------------+-------------------------------------------+
@@ -706,66 +720,66 @@ public static function renderProvider()
+---------------+---------------+-------------------------------------------+
TABLE
- ,
- ],
+ ,
+ ],
'TabeCellStyle with fg,bg. Also with rowspan and colspan > 1' => [
[],
[
- [
- new TableCell(
- '978>',
- [
- 'style' => new TableCellStyle([
- 'fg' => 'black',
- 'bg' => 'green',
- ]),
- ]
- ),
- 'De Monarchia',
- new TableCell(
- "Dante Alighieri \nspans multiple rows rows Dante Alighieri \nspans multiple rows rows",
- [
- 'rowspan' => 2,
- 'style' => new TableCellStyle([
- 'fg' => 'red',
- 'bg' => 'green',
- 'align' => 'center',
- ]),
- ]
- ),
- ],
-
- [
- '99921-58-10-7',
- 'Divine Comedy',
- ],
- new TableSeparator(),
- [
- new TableCell(
- 'test',
- [
- 'colspan' => 2,
- 'style' => new TableCellStyle([
- 'fg' => 'red',
- 'bg' => 'green',
- 'align' => 'center',
- ]),
- ]
- ),
- new TableCell(
- 'tttt',
- [
- 'style' => new TableCellStyle([
- 'fg' => 'red',
- 'bg' => 'green',
- 'align' => 'right',
- ]),
- ]
- ),
- ],
+ [
+ new TableCell(
+ '978>',
+ [
+ 'style' => new TableCellStyle([
+ 'fg' => 'black',
+ 'bg' => 'green',
+ ]),
+ ]
+ ),
+ 'De Monarchia',
+ new TableCell(
+ "Dante Alighieri \nspans multiple rows rows Dante Alighieri \nspans multiple rows rows",
+ [
+ 'rowspan' => 2,
+ 'style' => new TableCellStyle([
+ 'fg' => 'red',
+ 'bg' => 'green',
+ 'align' => 'center',
+ ]),
+ ]
+ ),
+ ],
+
+ [
+ '99921-58-10-7',
+ 'Divine Comedy',
+ ],
+ new TableSeparator(),
+ [
+ new TableCell(
+ 'test',
+ [
+ 'colspan' => 2,
+ 'style' => new TableCellStyle([
+ 'fg' => 'red',
+ 'bg' => 'green',
+ 'align' => 'center',
+ ]),
+ ]
+ ),
+ new TableCell(
+ 'tttt',
+ [
+ 'style' => new TableCellStyle([
+ 'fg' => 'red',
+ 'bg' => 'green',
+ 'align' => 'right',
+ ]),
+ ]
+ ),
+ ],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+---------------+---------------+-------------------------------------------+
[39;49m| [39;49m[31m978[39m[39;49m | De Monarchia |[39;49m[31;42m Dante Alighieri [39;49m[39;49m|[39;49m
[39;49m| [39;49m[32m99921-58-10-7[39m[39;49m | Divine Comedy |[39;49m[31;42m spans multiple rows rows Dante Alighieri [39;49m[39;49m|[39;49m
@@ -775,9 +789,9 @@ public static function renderProvider()
+---------------+---------------+-------------------------------------------+
TABLE
- ,
- true,
- ],
+ ,
+ true,
+ ],
'TabeCellStyle with cellFormat. Also with rowspan and colspan > 1' => [
[
new TableCell(
@@ -820,7 +834,7 @@ public static function renderProvider()
],
],
'default',
-<<<'TABLE'
+ <<<'TABLE'
+----------------+---------------+---------------------+
|[30;46m ISBN [39;49m|[32m Title [39m|[32m Author [39m|
+----------------+---------------+---------------------+
@@ -832,7 +846,7 @@ public static function renderProvider()
TABLE
,
true,
- ],
+ ],
];
}
@@ -1288,15 +1302,15 @@ public static function renderSetTitle()
TABLE
,
true,
- ],
+ ],
'header contains multiple lines' => [
'Multiline'."\n".'header'."\n".'here',
'footer',
'default',
<<<'TABLE'
-+---------------+---- Multiline
++---------------+--- Multiline
header
-here -+------------------+
+here +------------------+
| ISBN | Title | Author |
+---------------+--------------------------+------------------+
| 99921-58-10-7 | Divine Comedy | Dante Alighieri |
@@ -1305,7 +1319,7 @@ public static function renderSetTitle()
| 80-902734-1-6 | And Then There Were None | Agatha Christie |
+---------------+---------- footer --------+------------------+
-TABLE
+TABLE,
],
[
'Books',
@@ -1321,7 +1335,7 @@ public static function renderSetTitle()
│ 80-902734-1-6 │ And Then There Were None │ Agatha Christie │
└───────────────┴───────── Page 1/2 ───────┴──────────────────┘
-TABLE
+TABLE,
],
[
'Boooooooooooooooooooooooooooooooooooooooooooooooooooooooks',
@@ -1337,7 +1351,7 @@ public static function renderSetTitle()
| 80-902734-1-6 | And Then There Were None | Agatha Christie |
+- Page 1/99999999999999999999999999999999999999999999999... -+
-TABLE
+TABLE,
],
];
}
@@ -1558,35 +1572,35 @@ public function testWithColspanAndMaxWith()
$table->setColumnMaxWidth(1, 15);
$table->setColumnMaxWidth(2, 15);
$table->setRows([
- [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 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'],
- ])
+ [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 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'],
+ ])
;
$table->render();
$expected =
<<
[
+ << [
<<getOutputContent($output)
);
}
+
+ public function testGithubIssue60038WidthOfCellWithEmoji()
+ {
+ $table = (new Table($output = $this->getOutputStream()))
+ ->setHeaderTitle('Test Title')
+ ->setHeaders(['Title', 'Author'])
+ ->setRows([
+ ['🎭 💫 ☯ Divine Comedy', 'Dante Alighieri'],
+ // the snowflake (e2 9d 84 ef b8 8f) has a variant selector
+ ['👑 ❄️ 🗡 Game of Thrones', 'George R.R. Martin'],
+ // the snowflake in text style (e2 9d 84 ef b8 8e) has a variant selector
+ ['❄︎❄︎❄︎ snowflake in text style ❄︎❄︎❄︎', ''],
+ ['And a very long line to show difference in previous lines', ''],
+ ])
+ ;
+ $table->render();
+
+ $this->assertSame(<<
getOutputContent($output)
+ );
+ }
}
diff --git a/Tests/Helper/TreeHelperTest.php b/Tests/Helper/TreeHelperTest.php
new file mode 100644
index 000000000..5d1399b27
--- /dev/null
+++ b/Tests/Helper/TreeHelperTest.php
@@ -0,0 +1,364 @@
+
+ *
+ * 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\TreeHelper;
+use Symfony\Component\Console\Helper\TreeNode;
+use Symfony\Component\Console\Helper\TreeStyle;
+use Symfony\Component\Console\Output\BufferedOutput;
+
+class TreeHelperTest extends TestCase
+{
+ public function testRenderWithoutNode()
+ {
+ $output = new BufferedOutput();
+ $tree = TreeHelper::createTree($output);
+
+ $tree->render();
+ $this->assertSame(\PHP_EOL, $output->fetch());
+ }
+
+ public function testRenderSingleNode()
+ {
+ $rootNode = new TreeNode('Root');
+ $output = new BufferedOutput();
+ $tree = TreeHelper::createTree($output, $rootNode);
+
+ $tree->render();
+ $this->assertSame("Root\n", self::normalizeLineBreaks($output->fetch()));
+ }
+
+ public function testRenderTwoLevelTree()
+ {
+ $rootNode = new TreeNode('Root');
+ $child1 = new TreeNode('Child 1');
+ $child2 = new TreeNode('Child 2');
+
+ $rootNode->addChild($child1);
+ $rootNode->addChild($child2);
+
+ $output = new BufferedOutput();
+ $tree = TreeHelper::createTree($output, $rootNode);
+
+ $tree->render();
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testRenderThreeLevelTree()
+ {
+ $rootNode = new TreeNode('Root');
+ $child1 = new TreeNode('Child 1');
+ $child2 = new TreeNode('Child 2');
+ $subChild1 = new TreeNode('SubChild 1');
+
+ $child1->addChild($subChild1);
+ $rootNode->addChild($child1);
+ $rootNode->addChild($child2);
+
+ $output = new BufferedOutput();
+ $tree = TreeHelper::createTree($output, $rootNode);
+
+ $tree->render();
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testRenderMultiLevelTree()
+ {
+ $rootNode = new TreeNode('Root');
+ $child1 = new TreeNode('Child 1');
+ $child2 = new TreeNode('Child 2');
+ $subChild1 = new TreeNode('SubChild 1');
+ $subChild2 = new TreeNode('SubChild 2');
+ $subSubChild1 = new TreeNode('SubSubChild 1');
+
+ $subChild1->addChild($subSubChild1);
+ $child1->addChild($subChild1);
+ $child1->addChild($subChild2);
+ $rootNode->addChild($child1);
+ $rootNode->addChild($child2);
+
+ $output = new BufferedOutput();
+ $tree = TreeHelper::createTree($output, $rootNode);
+
+ $tree->render();
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testRenderSingleNodeTree()
+ {
+ $rootNode = new TreeNode('Root');
+ $output = new BufferedOutput();
+ $tree = TreeHelper::createTree($output, $rootNode);
+
+ $tree->render();
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testRenderEmptyTree()
+ {
+ $rootNode = new TreeNode('Root');
+ $output = new BufferedOutput();
+ $tree = TreeHelper::createTree($output, $rootNode);
+
+ $tree->render();
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testRenderDeeplyNestedTree()
+ {
+ $rootNode = new TreeNode('Root');
+ $current = $rootNode;
+ for ($i = 1; $i <= 10; ++$i) {
+ $child = new TreeNode("Level $i");
+ $current->addChild($child);
+ $current = $child;
+ }
+
+ $style = new TreeStyle(...[
+ '└── ',
+ '└── ',
+ '',
+ ' ',
+ ' ',
+ '',
+ ]);
+
+ $output = new BufferedOutput();
+ $tree = TreeHelper::createTree($output, $rootNode, [], $style);
+
+ $tree->render();
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testRenderNodeWithMultipleChildren()
+ {
+ $rootNode = new TreeNode('Root');
+ $child1 = new TreeNode('Child 1');
+ $child2 = new TreeNode('Child 2');
+ $child3 = new TreeNode('Child 3');
+
+ $rootNode->addChild($child1);
+ $rootNode->addChild($child2);
+ $rootNode->addChild($child3);
+
+ $output = new BufferedOutput();
+ $tree = TreeHelper::createTree($output, $rootNode);
+
+ $tree->render();
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testRenderNodeWithMultipleChildrenWithStringConversion()
+ {
+ $rootNode = new TreeNode('Root');
+
+ $rootNode->addChild('Child 1');
+ $rootNode->addChild('Child 2');
+ $rootNode->addChild('Child 3');
+
+ $output = new BufferedOutput();
+ $tree = TreeHelper::createTree($output, $rootNode);
+
+ $tree->render();
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testRenderTreeWithDuplicateNodeNames()
+ {
+ $rootNode = new TreeNode('Root');
+ $child1 = new TreeNode('Child');
+ $child2 = new TreeNode('Child');
+ $subChild1 = new TreeNode('Child');
+
+ $child1->addChild($subChild1);
+ $rootNode->addChild($child1);
+ $rootNode->addChild($child2);
+
+ $output = new BufferedOutput();
+ $tree = TreeHelper::createTree($output, $rootNode);
+
+ $tree->render();
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testRenderTreeWithComplexNodeNames()
+ {
+ $rootNode = new TreeNode('Root');
+ $child1 = new TreeNode('Child 1 (special)');
+ $child2 = new TreeNode('Child_2@#$');
+ $subChild1 = new TreeNode('Node with spaces');
+
+ $child1->addChild($subChild1);
+ $rootNode->addChild($child1);
+ $rootNode->addChild($child2);
+
+ $output = new BufferedOutput();
+ $tree = TreeHelper::createTree($output, $rootNode);
+
+ $tree->render();
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testRenderTreeWithCycle()
+ {
+ $rootNode = new TreeNode('Root');
+ $child1 = new TreeNode('Child 1');
+ $child2 = new TreeNode('Child 2');
+
+ $child1->addChild($child2);
+ // Create a cycle voluntarily
+ $child2->addChild($child1);
+
+ $rootNode->addChild($child1);
+
+ $output = new BufferedOutput();
+ $tree = TreeHelper::createTree($output, $rootNode);
+
+ $this->expectException(\LogicException::class);
+ $tree->render();
+ }
+
+ public function testRenderWideTree()
+ {
+ $rootNode = new TreeNode('Root');
+ for ($i = 1; $i <= 100; ++$i) {
+ $rootNode->addChild(new TreeNode("Child $i"));
+ }
+
+ $output = new BufferedOutput();
+
+ $tree = TreeHelper::createTree($output, $rootNode);
+ $tree->render();
+
+ $lines = explode("\n", self::normalizeLineBreaks(trim($output->fetch())));
+ $this->assertCount(101, $lines);
+ $this->assertSame('Root', $lines[0]);
+ $this->assertSame('└── Child 100', end($lines));
+ }
+
+ public function testCreateWithRoot()
+ {
+ $output = new BufferedOutput();
+ $array = ['child1', 'child2'];
+
+ $tree = TreeHelper::createTree($output, 'root', $array);
+
+ $tree->render();
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testCreateWithNestedArray()
+ {
+ $output = new BufferedOutput();
+ $array = ['child1', 'child2' => ['child2.1', 'child2.2' => ['child2.2.1']], 'child3'];
+
+ $tree = TreeHelper::createTree($output, 'root', $array);
+
+ $tree->render();
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testCreateWithoutRoot()
+ {
+ $output = new BufferedOutput();
+ $array = ['child1', 'child2'];
+
+ $tree = TreeHelper::createTree($output, null, $array);
+
+ $tree->render();
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testCreateWithEmptyArray()
+ {
+ $output = new BufferedOutput();
+ $array = [];
+
+ $tree = TreeHelper::createTree($output, null, $array);
+
+ $tree->render();
+ $this->assertSame('', self::normalizeLineBreaks(trim($output->fetch())));
+ }
+
+ private static function normalizeLineBreaks($text)
+ {
+ return str_replace(\PHP_EOL, "\n", $text);
+ }
+}
diff --git a/Tests/Helper/TreeNodeTest.php b/Tests/Helper/TreeNodeTest.php
new file mode 100644
index 000000000..0e80da3bd
--- /dev/null
+++ b/Tests/Helper/TreeNodeTest.php
@@ -0,0 +1,86 @@
+
+ *
+ * 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\TreeNode;
+
+class TreeNodeTest extends TestCase
+{
+ public function testNodeInitialization()
+ {
+ $node = new TreeNode('Root');
+ $this->assertSame('Root', $node->getValue());
+ $this->assertSame(0, iterator_count($node->getChildren()));
+ }
+
+ public function testAddingChildren()
+ {
+ $root = new TreeNode('Root');
+ $child = new TreeNode('Child');
+
+ $root->addChild($child);
+
+ $this->assertSame(1, iterator_count($root->getChildren()));
+ $this->assertSame($child, iterator_to_array($root->getChildren())[0]);
+ }
+
+ public function testAddingChildrenAsString()
+ {
+ $root = new TreeNode('Root');
+
+ $root->addChild('Child 1');
+ $root->addChild('Child 2');
+
+ $this->assertSame(2, iterator_count($root->getChildren()));
+
+ $children = iterator_to_array($root->getChildren());
+
+ $this->assertSame(0, iterator_count($children[0]->getChildren()));
+ $this->assertSame(0, iterator_count($children[1]->getChildren()));
+
+ $this->assertSame('Child 1', $children[0]->getValue());
+ $this->assertSame('Child 2', $children[1]->getValue());
+ }
+
+ public function testAddingChildrenWithGenerators()
+ {
+ $root = new TreeNode('Root');
+
+ $root->addChild(function () {
+ yield new TreeNode('Generated Child 1');
+ yield new TreeNode('Generated Child 2');
+ });
+
+ $this->assertSame(2, iterator_count($root->getChildren()));
+
+ $children = iterator_to_array($root->getChildren());
+
+ $this->assertSame('Generated Child 1', $children[0]->getValue());
+ $this->assertSame('Generated Child 2', $children[1]->getValue());
+ }
+
+ public function testRecursiveStructure()
+ {
+ $root = new TreeNode('Root');
+ $child1 = new TreeNode('Child 1');
+ $child2 = new TreeNode('Child 2');
+ $leaf1 = new TreeNode('Leaf 1');
+
+ $child1->addChild($leaf1);
+ $root->addChild($child1);
+ $root->addChild($child2);
+
+ $this->assertSame(2, iterator_count($root->getChildren()));
+ $this->assertSame($leaf1, iterator_to_array($child1->getChildren())[0]);
+ }
+}
diff --git a/Tests/Helper/TreeStyleTest.php b/Tests/Helper/TreeStyleTest.php
new file mode 100644
index 000000000..7f5bfedd3
--- /dev/null
+++ b/Tests/Helper/TreeStyleTest.php
@@ -0,0 +1,231 @@
+
+ *
+ * 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\TreeHelper;
+use Symfony\Component\Console\Helper\TreeNode;
+use Symfony\Component\Console\Helper\TreeStyle;
+use Symfony\Component\Console\Output\BufferedOutput;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class TreeStyleTest extends TestCase
+{
+ public function testDefaultStyle()
+ {
+ $output = new BufferedOutput();
+ $tree = self::createTree($output);
+
+ $tree->render();
+
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testBoxStyle()
+ {
+ $output = new BufferedOutput();
+ $this->createTree($output, TreeStyle::box())->render();
+
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testBoxDoubleStyle()
+ {
+ $output = new BufferedOutput();
+ $this->createTree($output, TreeStyle::boxDouble())->render();
+
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testCompactStyle()
+ {
+ $output = new BufferedOutput();
+ $this->createTree($output, TreeStyle::compact())->render();
+
+ $this->assertSame(<<<'TREE'
+root
+├ A
+│ ├ A1
+│ └ A2
+│ └ A2.1
+│ ├ A2.1.1
+│ └ A2.1.2
+├ B
+│ ├ B1
+│ │ ├ B11
+│ │ └ B12
+│ └ B2
+└ C
+TREE, self::normalizeLineBreaks(trim($output->fetch())));
+ }
+
+ public function testLightStyle()
+ {
+ $output = new BufferedOutput();
+ $this->createTree($output, TreeStyle::light())->render();
+
+ $this->assertSame(<<<'TREE'
+root
+|-- A
+| |-- A1
+| `-- A2
+| `-- A2.1
+| |-- A2.1.1
+| `-- A2.1.2
+|-- B
+| |-- B1
+| | |-- B11
+| | `-- B12
+| `-- B2
+`-- C
+TREE, self::normalizeLineBreaks(trim($output->fetch())));
+ }
+
+ public function testMinimalStyle()
+ {
+ $output = new BufferedOutput();
+ $this->createTree($output, TreeStyle::minimal())->render();
+
+ $this->assertSame(<<<'TREE'
+root
+. A
+. . A1
+. . A2
+. . A2.1
+. . A2.1.1
+. . A2.1.2
+. B
+. . B1
+. . . B11
+. . . B12
+. . B2
+. C
+TREE, self::normalizeLineBreaks(trim($output->fetch())));
+ }
+
+ public function testRoundedStyle()
+ {
+ $output = new BufferedOutput();
+ $this->createTree($output, TreeStyle::rounded())->render();
+
+ $this->assertSame(<<<'TREE'
+root
+├─ A
+│ ├─ A1
+│ ╰─ A2
+│ ╰─ A2.1
+│ ├─ A2.1.1
+│ ╰─ A2.1.2
+├─ B
+│ ├─ B1
+│ │ ├─ B11
+│ │ ╰─ B12
+│ ╰─ B2
+╰─ C
+TREE, self::normalizeLineBreaks(trim($output->fetch())));
+ }
+
+ public function testCustomPrefix()
+ {
+ $style = new TreeStyle('A ', 'B ', 'C ', 'D ', 'E ', 'F ');
+ $output = new BufferedOutput();
+ self::createTree($output, $style)->render();
+
+ $this->assertSame(<<<'TREE'
+root
+C A F A
+C D A F A1
+C D B F A2
+C D E B F A2.1
+C D E E A F A2.1.1
+C D E E B F A2.1.2
+C A F B
+C D A F B1
+C D D A F B11
+C D D B F B12
+C D B F B2
+C B F C
+TREE, self::normalizeLineBreaks(trim($output->fetch())));
+ }
+
+ private static function createTree(OutputInterface $output, ?TreeStyle $style = null): TreeHelper
+ {
+ $root = new TreeNode('root');
+ $root
+ ->addChild((new TreeNode('A'))
+ ->addChild(new TreeNode('A1'))
+ ->addChild((new TreeNode('A2'))
+ ->addChild((new TreeNode('A2.1'))
+ ->addChild(new TreeNode('A2.1.1'))
+ ->addChild(new TreeNode('A2.1.2'))
+ )
+ )
+ )
+ ->addChild((new TreeNode('B'))
+ ->addChild((new TreeNode('B1'))
+ ->addChild(new TreeNode('B11'))
+ ->addChild(new TreeNode('B12'))
+ )
+ ->addChild(new TreeNode('B2'))
+ )
+ ->addChild(new TreeNode('C'));
+
+ return TreeHelper::createTree($output, $root, [], $style);
+ }
+
+ private static function normalizeLineBreaks($text)
+ {
+ return str_replace(\PHP_EOL, "\n", $text);
+ }
+}
diff --git a/Tests/Input/ArgvInputTest.php b/Tests/Input/ArgvInputTest.php
index a47d557b7..0e76f9ee6 100644
--- a/Tests/Input/ArgvInputTest.php
+++ b/Tests/Input/ArgvInputTest.php
@@ -26,17 +26,17 @@ public function testConstructor()
$r = new \ReflectionObject($input);
$p = $r->getProperty('tokens');
- $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');
}
/**
@@ -57,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()
@@ -324,6 +324,11 @@ public static function provideInvalidInput(): array
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".',
+ ],
];
}
@@ -358,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()
@@ -366,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)]));
@@ -388,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()
@@ -401,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()
@@ -410,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')]));
@@ -490,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', '']));
}
@@ -498,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);
}
/**
@@ -510,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()
@@ -534,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 d6fe32bb3..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,7 +71,7 @@ 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(): array
@@ -162,7 +162,7 @@ public static function provideInvalidInput(): array
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 05447426c..a9d612f97 100644
--- a/Tests/Input/InputArgumentTest.php
+++ b/Tests/Input/InputArgumentTest.php
@@ -23,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()
@@ -62,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()
@@ -77,11 +77,11 @@ 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()
diff --git a/Tests/Input/InputDefinitionTest.php b/Tests/Input/InputDefinitionTest.php
index 470f3ca74..ab203e6e5 100644
--- a/Tests/Input/InputDefinitionTest.php
+++ b/Tests/Input/InputDefinitionTest.php
@@ -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 74bf69586..47ab503f7 100644
--- a/Tests/Input/InputOptionTest.php
+++ b/Tests/Input/InputOptionTest.php
@@ -23,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()
@@ -52,13 +52,29 @@ 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', '');
+ $this->assertNull($option->getShortcut(), '__construct() makes the shortcut null when given an empty string');
+ $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->assertSame('f|fff', $option->getShortcut(), '__construct() removes empty shortcuts');
+ $option = new InputOption('foo', 'f||fff');
+ $this->assertSame('f|fff', $option->getShortcut(), '__construct() removes empty shortcuts');
+ $option = new InputOption('foo', '0');
+ $this->assertSame('0', $option->getShortcut(), '-0 is an acceptable shortcut value');
+ $option = new InputOption('foo', ['0', 'z']);
+ $this->assertSame('0|z', $option->getShortcut(), '-0 is an acceptable shortcut value when embedded in an array');
+ $option = new InputOption('foo', '0|z');
+ $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');
}
public function testModes()
@@ -126,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');
@@ -153,11 +169,11 @@ 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()
diff --git a/Tests/Input/InputTest.php b/Tests/Input/InputTest.php
index 34fb4833b..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'));
@@ -84,15 +84,15 @@ public function testGetInvalidOption()
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()
diff --git a/Tests/Input/StringInputTest.php b/Tests/Input/StringInputTest.php
index 338c1428a..92425daab 100644
--- a/Tests/Input/StringInputTest.php
+++ b/Tests/Input/StringInputTest.php
@@ -27,7 +27,7 @@ public function testTokenize($input, $tokens, $message)
$input = new StringInput($input);
$r = new \ReflectionClass(ArgvInput::class);
$p = $r->getProperty('tokens');
- $this->assertEquals($tokens, $p->getValue($input), $message);
+ $this->assertSame($tokens, $p->getValue($input), $message);
}
public function testInputOptionWithGivenString()
@@ -39,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()
@@ -77,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 43d779631..0464c8c5f 100644
--- a/Tests/Logger/ConsoleLoggerTest.php
+++ b/Tests/Logger/ConsoleLoggerTest.php
@@ -151,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);
diff --git a/Tests/Messenger/RunCommandMessageHandlerTest.php b/Tests/Messenger/RunCommandMessageHandlerTest.php
index adc31e0ec..898492374 100644
--- a/Tests/Messenger/RunCommandMessageHandlerTest.php
+++ b/Tests/Messenger/RunCommandMessageHandlerTest.php
@@ -20,6 +20,10 @@
use Symfony\Component\Console\Messenger\RunCommandMessage;
use Symfony\Component\Console\Messenger\RunCommandMessageHandler;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Messenger\Exception\RecoverableExceptionInterface;
+use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException;
+use Symfony\Component\Messenger\Exception\UnrecoverableExceptionInterface;
+use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
/**
* @author Kevin Bond
@@ -81,17 +85,51 @@ public function testThrowOnNonSuccess()
$this->fail('Exception not thrown.');
}
+ public function testExecutesCommandThatThrownUnrecoverableException()
+ {
+ $handler = new RunCommandMessageHandler($this->createApplicationWithCommand());
+
+ try {
+ $handler(new RunCommandMessage('test:command --throw-unrecoverable'));
+ } catch (UnrecoverableExceptionInterface $e) {
+ $this->assertSame('Unrecoverable exception message', $e->getMessage());
+ $this->assertNull($e->getPrevious());
+
+ return;
+ }
+
+ $this->fail('Exception not thrown.');
+ }
+
+ public function testExecutesCommandThatThrownRecoverableException()
+ {
+ $handler = new RunCommandMessageHandler($this->createApplicationWithCommand());
+
+ try {
+ $handler(new RunCommandMessage('test:command --throw-recoverable'));
+ } catch (RecoverableExceptionInterface $e) {
+ $this->assertSame('Recoverable exception message', $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 {
+ new class extends Command {
public function configure(): void
{
$this
->setName('test:command')
->addOption('throw')
+ ->addOption('throw-unrecoverable')
+ ->addOption('throw-recoverable')
->addOption('exit', null, InputOption::VALUE_REQUIRED, 0)
;
}
@@ -100,6 +138,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->write('some message');
+ if ($input->getOption('throw-unrecoverable')) {
+ throw new UnrecoverableMessageHandlingException('Unrecoverable exception message');
+ }
+
+ if ($input->getOption('throw-recoverable')) {
+ throw new RecoverableMessageHandlingException('Recoverable exception message');
+ }
+
if ($input->getOption('throw')) {
throw new \RuntimeException('exception message');
}
diff --git a/Tests/Output/ConsoleSectionOutputTest.php b/Tests/Output/ConsoleSectionOutputTest.php
index e50f8d54a..6d2fa6c3c 100644
--- a/Tests/Output/ConsoleSectionOutputTest.php
+++ b/Tests/Output/ConsoleSectionOutputTest.php
@@ -44,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()
@@ -56,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()
@@ -197,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()
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 8a1e2840e..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'],
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 6e8053a35..15d8212b9 100644
--- a/Tests/Question/QuestionTest.php
+++ b/Tests/Question/QuestionTest.php
@@ -20,7 +20,6 @@ class QuestionTest extends TestCase
protected function setUp(): void
{
- parent::setUp();
$this->question = new Question('Test question');
}
@@ -157,7 +156,7 @@ public function testSetAutocompleterValuesInvalid($values)
public function testSetAutocompleterValuesWithTraversable()
{
$question1 = new Question('Test question 1');
- $iterator1 = $this->getMockForAbstractClass(\IteratorAggregate::class);
+ $iterator1 = $this->createMock(\IteratorAggregate::class);
$iterator1
->expects($this->once())
->method('getIterator')
@@ -165,7 +164,7 @@ public function testSetAutocompleterValuesWithTraversable()
$question1->setAutocompleterValues($iterator1);
$question2 = new Question('Test question 2');
- $iterator2 = $this->getMockForAbstractClass(\IteratorAggregate::class);
+ $iterator2 = $this->createMock(\IteratorAggregate::class);
$iterator2
->expects($this->once())
->method('getIterator')
diff --git a/Tests/SignalRegistry/SignalMapTest.php b/Tests/SignalRegistry/SignalMapTest.php
index 887c5d7af..3a0c49bb0 100644
--- a/Tests/SignalRegistry/SignalMapTest.php
+++ b/Tests/SignalRegistry/SignalMapTest.php
@@ -18,14 +18,13 @@ class SignalMapTest extends TestCase
{
/**
* @requires extension pcntl
- *
- * @testWith [2, "SIGINT"]
- * [9, "SIGKILL"]
- * [15, "SIGTERM"]
*/
- public function testSignalExists(int $signal, string $expected)
+ public function testSignalExists()
{
- $this->assertSame($expected, SignalMap::getSignalName($signal));
+ $this->assertSame('SIGINT', SignalMap::getSignalName(\SIGINT));
+ $this->assertSame('SIGKILL', SignalMap::getSignalName(\SIGKILL));
+ $this->assertSame('SIGTERM', SignalMap::getSignalName(\SIGTERM));
+ $this->assertSame('SIGSYS', SignalMap::getSignalName(\SIGSYS));
}
public function testSignalDoesNotExist()
diff --git a/Tests/SignalRegistry/SignalRegistryTest.php b/Tests/SignalRegistry/SignalRegistryTest.php
index 4cb435416..92d500f9e 100644
--- a/Tests/SignalRegistry/SignalRegistryTest.php
+++ b/Tests/SignalRegistry/SignalRegistryTest.php
@@ -23,12 +23,11 @@ protected function tearDown(): void
{
pcntl_async_signals(false);
// 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);
}
public function testOneCallbackForASignalSignalIsHandled()
diff --git a/Tests/Style/SymfonyStyleTest.php b/Tests/Style/SymfonyStyleTest.php
index 0b40c7c3f..a3b7ae406 100644
--- a/Tests/Style/SymfonyStyleTest.php
+++ b/Tests/Style/SymfonyStyleTest.php
@@ -15,9 +15,11 @@
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
+use Symfony\Component\Console\Helper\TreeHelper;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\Input;
use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\NullOutput;
@@ -154,6 +156,99 @@ public function testCreateTableWithoutConsoleOutput()
$style->createTable()->appendRow(['row']);
}
+ public function testCreateTree()
+ {
+ $output = $this->createMock(OutputInterface::class);
+ $output
+ ->method('getFormatter')
+ ->willReturn(new OutputFormatter());
+
+ $style = new SymfonyStyle($this->createMock(InputInterface::class), $output);
+
+ $tree = $style->createTree([]);
+ $this->assertInstanceOf(TreeHelper::class, $tree);
+ }
+
+ public function testTree()
+ {
+ $input = $this->createMock(InputInterface::class);
+ $output = new BufferedOutput();
+ $style = new SymfonyStyle($input, $output);
+
+ $tree = $style->createTree(['A', 'B' => ['B1' => ['B11', 'B12'], 'B2'], 'C'], 'root');
+ $tree->render();
+
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testCreateTreeWithArray()
+ {
+ $input = $this->createMock(InputInterface::class);
+ $output = new BufferedOutput();
+ $style = new SymfonyStyle($input, $output);
+
+ $tree = $style->createTree(['A', 'B' => ['B1' => ['B11', 'B12'], 'B2'], 'C'], 'root');
+ $tree->render();
+
+ $this->assertSame($tree = <<fetch())));
+ }
+
+ public function testCreateTreeWithIterable()
+ {
+ $input = $this->createMock(InputInterface::class);
+ $output = new BufferedOutput();
+ $style = new SymfonyStyle($input, $output);
+
+ $tree = $style->createTree(new \ArrayIterator(['A', 'B' => ['B1' => ['B11', 'B12'], 'B2'], 'C']), 'root');
+ $tree->render();
+
+ $this->assertSame(<<fetch())));
+ }
+
+ public function testCreateTreeWithConsoleOutput()
+ {
+ $input = $this->createMock(InputInterface::class);
+ $output = $this->createMock(ConsoleOutputInterface::class);
+ $output
+ ->method('getFormatter')
+ ->willReturn(new OutputFormatter());
+ $output
+ ->expects($this->once())
+ ->method('section')
+ ->willReturn($this->createMock(ConsoleSectionOutput::class));
+
+ $style = new SymfonyStyle($input, $output);
+
+ $style->createTree([]);
+ }
+
public function testGetErrorStyleUsesTheCurrentOutputIfNoErrorOutputIsAvailable()
{
$output = $this->createMock(OutputInterface::class);
@@ -219,4 +314,9 @@ public function testAskAndClearExpectFullSectionCleared()
escapeshellcmd(stream_get_contents($output->getStream()))
);
}
+
+ private static function normalizeLineBreaks($text)
+ {
+ return str_replace(\PHP_EOL, "\n", $text);
+ }
}
diff --git a/Tests/Tester/ApplicationTesterTest.php b/Tests/Tester/ApplicationTesterTest.php
index f43775179..843f2eac7 100644
--- a/Tests/Tester/ApplicationTesterTest.php
+++ b/Tests/Tester/ApplicationTesterTest.php
@@ -14,7 +14,9 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\Output;
+use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Tester\ApplicationTester;
@@ -29,8 +31,10 @@ protected function setUp(): void
$this->application->setAutoExit(false);
$this->application->register('foo')
->addArgument('foo')
- ->setCode(function ($input, $output) {
+ ->setCode(function (OutputInterface $output): int {
$output->writeln('foo');
+
+ return 0;
})
;
@@ -65,11 +69,13 @@ public function testSetInputs()
{
$application = new Application();
$application->setAutoExit(false);
- $application->register('foo')->setCode(function ($input, $output) {
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output): int {
$helper = new QuestionHelper();
$helper->ask($input, $output, new Question('Q1'));
$helper->ask($input, $output, new Question('Q2'));
$helper->ask($input, $output, new Question('Q3'));
+
+ return 0;
});
$tester = new ApplicationTester($application);
@@ -91,8 +97,10 @@ public function testErrorOutput()
$application->setAutoExit(false);
$application->register('foo')
->addArgument('foo')
- ->setCode(function ($input, $output) {
+ ->setCode(function (OutputInterface $output): int {
$output->getErrorOutput()->write('foo');
+
+ return 0;
})
;
diff --git a/Tests/Tester/CommandTesterTest.php b/Tests/Tester/CommandTesterTest.php
index ce0a24b99..d36042038 100644
--- a/Tests/Tester/CommandTesterTest.php
+++ b/Tests/Tester/CommandTesterTest.php
@@ -16,7 +16,9 @@
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\Output;
+use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
@@ -32,7 +34,11 @@ protected function setUp(): void
$this->command = new Command('foo');
$this->command->addArgument('command');
$this->command->addArgument('foo');
- $this->command->setCode(function ($input, $output) { $output->writeln('foo'); });
+ $this->command->setCode(function (OutputInterface $output): int {
+ $output->writeln('foo');
+
+ return 0;
+ });
$this->tester = new CommandTester($this->command);
$this->tester->execute(['foo' => 'bar'], ['interactive' => false, 'decorated' => false, 'verbosity' => Output::VERBOSITY_VERBOSE]);
@@ -92,7 +98,11 @@ public function testCommandFromApplication()
$application->setAutoExit(false);
$command = new Command('foo');
- $command->setCode(function ($input, $output) { $output->writeln('foo'); });
+ $command->setCode(function (OutputInterface $output): int {
+ $output->writeln('foo');
+
+ return 0;
+ });
$application->add($command);
@@ -112,11 +122,13 @@ public function testCommandWithInputs()
$command = new Command('foo');
$command->setHelperSet(new HelperSet([new QuestionHelper()]));
- $command->setCode(function ($input, $output) use ($questions, $command) {
+ $command->setCode(function (InputInterface $input, OutputInterface $output) use ($questions, $command): int {
$helper = $command->getHelper('question');
$helper->ask($input, $output, new Question($questions[0]));
$helper->ask($input, $output, new Question($questions[1]));
$helper->ask($input, $output, new Question($questions[2]));
+
+ return 0;
});
$tester = new CommandTester($command);
@@ -127,6 +139,32 @@ public function testCommandWithInputs()
$this->assertEquals(implode('', $questions), $tester->getDisplay(true));
}
+ public function testCommandWithMultilineInputs()
+ {
+ $question = 'What is your address?';
+
+ $command = new Command('foo');
+ $command->setHelperSet(new HelperSet([new QuestionHelper()]));
+ $command->setCode(function (InputInterface $input, OutputInterface $output) use ($question, $command): int {
+ $output->write($command->getHelper('question')->ask($input, $output, (new Question($question))->setMultiline(true)));
+ $output->write(stream_get_contents($input->getStream()));
+
+ return 0;
+ });
+
+ $tester = new CommandTester($command);
+
+ $address = <<setInputs([$address."\x04", $address]);
+ $tester->execute([]);
+
+ $tester->assertCommandIsSuccessful();
+ $this->assertSame($question.$address.$address.\PHP_EOL, $tester->getDisplay());
+ }
+
public function testCommandWithDefaultInputs()
{
$questions = [
@@ -137,11 +175,13 @@ public function testCommandWithDefaultInputs()
$command = new Command('foo');
$command->setHelperSet(new HelperSet([new QuestionHelper()]));
- $command->setCode(function ($input, $output) use ($questions, $command) {
+ $command->setCode(function (InputInterface $input, OutputInterface $output) use ($questions, $command): int {
$helper = $command->getHelper('question');
$helper->ask($input, $output, new Question($questions[0], 'Bobby'));
$helper->ask($input, $output, new Question($questions[1], 'Fine'));
$helper->ask($input, $output, new Question($questions[2], 'France'));
+
+ return 0;
});
$tester = new CommandTester($command);
@@ -162,12 +202,14 @@ public function testCommandWithWrongInputsNumber()
$command = new Command('foo');
$command->setHelperSet(new HelperSet([new QuestionHelper()]));
- $command->setCode(function ($input, $output) use ($questions, $command) {
+ $command->setCode(function (InputInterface $input, OutputInterface $output) use ($questions, $command): int {
$helper = $command->getHelper('question');
$helper->ask($input, $output, new ChoiceQuestion('choice', ['a', 'b']));
$helper->ask($input, $output, new Question($questions[0]));
$helper->ask($input, $output, new Question($questions[1]));
$helper->ask($input, $output, new Question($questions[2]));
+
+ return 0;
});
$tester = new CommandTester($command);
@@ -189,12 +231,14 @@ public function testCommandWithQuestionsButNoInputs()
$command = new Command('foo');
$command->setHelperSet(new HelperSet([new QuestionHelper()]));
- $command->setCode(function ($input, $output) use ($questions, $command) {
+ $command->setCode(function (InputInterface $input, OutputInterface $output) use ($questions, $command): int {
$helper = $command->getHelper('question');
$helper->ask($input, $output, new ChoiceQuestion('choice', ['a', 'b']));
$helper->ask($input, $output, new Question($questions[0]));
$helper->ask($input, $output, new Question($questions[1]));
$helper->ask($input, $output, new Question($questions[2]));
+
+ return 0;
});
$tester = new CommandTester($command);
@@ -214,11 +258,13 @@ public function testSymfonyStyleCommandWithInputs()
];
$command = new Command('foo');
- $command->setCode(function ($input, $output) use ($questions) {
+ $command->setCode(function (InputInterface $input, OutputInterface $output) use ($questions): int {
$io = new SymfonyStyle($input, $output);
$io->ask($questions[0]);
$io->ask($questions[1]);
$io->ask($questions[2]);
+
+ return 0;
});
$tester = new CommandTester($command);
@@ -233,8 +279,10 @@ public function testErrorOutput()
$command = new Command('foo');
$command->addArgument('command');
$command->addArgument('foo');
- $command->setCode(function ($input, $output) {
+ $command->setCode(function (OutputInterface $output): int {
$output->getErrorOutput()->write('foo');
+
+ return 0;
});
$tester = new CommandTester($command);
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..c2cf3edc7
--- /dev/null
+++ b/Tests/phpt/alarm/command_exit.phpt
@@ -0,0 +1,63 @@
+--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
index fde3793a8..e14f80c47 100644
--- a/Tests/phpt/signal/command_exit.phpt
+++ b/Tests/phpt/signal/command_exit.phpt
@@ -1,13 +1,12 @@
--TEST--
-Test command that exist
+Test command that exits
--SKIPIF--
-
+
--FILE--
register('app')
- ->setCode(function(InputInterface $input, OutputInterface $output) {
+ ->setCode(function(InputInterface $input, OutputInterface $output): int {
$output->writeln((new QuestionHelper())->ask($input, $output, new Question('Foo?', 'foo')));
$output->writeln((new QuestionHelper())->ask($input, $output, new Question('Bar?', 'bar')));
+
+ return 0;
})
->getApplication()
->setDefaultCommand('app', true)
diff --git a/composer.json b/composer.json
index 0ed1bd9af..65d69913a 100644
--- a/composer.json
+++ b/composer.json
@@ -17,9 +17,10 @@
],
"require": {
"php": ">=8.2",
+ "symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-mbstring": "~1.0",
"symfony/service-contracts": "^2.5|^3",
- "symfony/string": "^6.4|^7.0"
+ "symfony/string": "^7.2"
},
"require-dev": {
"symfony/config": "^6.4|^7.0",