diff --git a/Application.php b/Application.php index f61761df5..36eb144c4 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]; } } @@ -111,10 +110,7 @@ public function setDispatcher(EventDispatcherInterface $dispatcher): void $this->dispatcher = $dispatcher; } - /** - * @return void - */ - public function setCommandLoader(CommandLoaderInterface $commandLoader) + public function setCommandLoader(CommandLoaderInterface $commandLoader): void { $this->commandLoader = $commandLoader; } @@ -128,12 +124,33 @@ public function getSignalRegistry(): SignalRegistry return $this->signalRegistry; } + public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent): void + { + $this->signalsToDispatchEvent = $signalsToDispatchEvent; + } + /** - * @return void + * Sets the interval to schedule a SIGALRM signal in seconds. */ - public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent) + public function setAlarmInterval(?int $seconds): void { - $this->signalsToDispatchEvent = $signalsToDispatchEvent; + $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); + } } /** @@ -169,6 +186,9 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu } } + $empty = new \stdClass(); + $prevShellVerbosity = [$_ENV['SHELL_VERBOSITY'] ?? $empty, $_SERVER['SHELL_VERBOSITY'] ?? $empty, getenv('SHELL_VERBOSITY')]; + try { $this->configureIO($input, $output); @@ -206,6 +226,18 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu $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 ($empty === $_ENV['SHELL_VERBOSITY'] = $prevShellVerbosity[0]) { + unset($_ENV['SHELL_VERBOSITY']); + } + if ($empty === $_SERVER['SHELL_VERBOSITY'] = $prevShellVerbosity[1]) { + unset($_SERVER['SHELL_VERBOSITY']); + } + if (\function_exists('putenv')) { + @putenv('SHELL_VERBOSITY'.(false === ($prevShellVerbosity[2] ?? false) ? '' : '='.$prevShellVerbosity[2])); + } } if ($this->autoExit) { @@ -224,7 +256,7 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu * * @return int 0 if everything went fine, or an error code */ - public function doRun(InputInterface $input, OutputInterface $output) + public function doRun(InputInterface $input, OutputInterface $output): int { if (true === $input->hasParameterOption(['--version', '-V'], true)) { $output->writeln($this->getLongVersion()); @@ -327,17 +359,11 @@ public function doRun(InputInterface $input, OutputInterface $output) return $exitCode; } - /** - * @return void - */ - public function reset() + public function reset(): void { } - /** - * @return void - */ - public function setHelperSet(HelperSet $helperSet) + public function setHelperSet(HelperSet $helperSet): void { $this->helperSet = $helperSet; } @@ -350,10 +376,7 @@ public function getHelperSet(): HelperSet return $this->helperSet ??= $this->getDefaultHelperSet(); } - /** - * @return void - */ - public function setDefinition(InputDefinition $definition) + public function setDefinition(InputDefinition $definition): void { $this->definition = $definition; } @@ -400,8 +423,6 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) { $suggestions->suggestOptions($this->getDefinition()->getOptions()); - - return; } if ( @@ -432,10 +453,8 @@ public function areExceptionsCaught(): bool /** * Sets whether to catch exceptions or not during commands execution. - * - * @return void */ - public function setCatchExceptions(bool $boolean) + public function setCatchExceptions(bool $boolean): void { $this->catchExceptions = $boolean; } @@ -458,10 +477,8 @@ public function isAutoExitEnabled(): bool /** * Sets whether to automatically exit after a command execution or not. - * - * @return void */ - public function setAutoExit(bool $boolean) + public function setAutoExit(bool $boolean): void { $this->autoExit = $boolean; } @@ -476,10 +493,8 @@ public function getName(): string /** * Sets the application name. - * - * @return void */ - public function setName(string $name) + public function setName(string $name): void { $this->name = $name; } @@ -494,20 +509,16 @@ public function getVersion(): string /** * Sets the application version. - * - * @return void */ - public function setVersion(string $version) + public function setVersion(string $version): void { $this->version = $version; } /** * Returns the long version of the application. - * - * @return string */ - public function getLongVersion() + public function getLongVersion(): string { if ('UNKNOWN' !== $this->getName()) { if ('UNKNOWN' !== $this->getVersion()) { @@ -534,10 +545,8 @@ public function register(string $name): Command * If a Command is not enabled it will not be added. * * @param Command[] $commands An array of commands - * - * @return void */ - public function addCommands(array $commands) + public function addCommands(array $commands): void { foreach ($commands as $command) { $this->add($command); @@ -549,10 +558,8 @@ public function addCommands(array $commands) * * If a command with the same name already exists, it will be overridden. * If the command is not enabled it will not be added. - * - * @return Command|null */ - public function add(Command $command) + public function add(Command $command): ?Command { $this->init(); @@ -585,11 +592,9 @@ public function add(Command $command) /** * Returns a registered command by name or alias. * - * @return Command - * * @throws CommandNotFoundException When given command name does not exist */ - public function get(string $name) + public function get(string $name): Command { $this->init(); @@ -662,7 +667,7 @@ public function findNamespace(string $namespace): string $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*'; $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces); - if (empty($namespaces)) { + if (!$namespaces) { $message = \sprintf('There are no commands defined in the "%s" namespace.', $namespace); if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { @@ -692,11 +697,9 @@ public function findNamespace(string $namespace): string * Contrary to get, this command tries to find the best * match if you give it an abbreviation of a name or alias. * - * @return Command - * * @throws CommandNotFoundException When command name is incorrect or ambiguous */ - public function find(string $name) + public function find(string $name): Command { $this->init(); @@ -718,12 +721,12 @@ public function find(string $name) $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $name))).'[^:]*'; $commands = preg_grep('{^'.$expr.'}', $allCommands); - if (empty($commands)) { + if (!$commands) { $commands = preg_grep('{^'.$expr.'}i', $allCommands); } // if no commands matched or we just matched namespaces - if (empty($commands) || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) { + if (!$commands || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) { if (false !== $pos = strrpos($name, ':')) { // check if a namespace exists and contains commands $this->findNamespace(substr($name, 0, $pos)); @@ -757,7 +760,7 @@ public function find(string $name) $aliases[$nameOrAlias] = $commandName; - return $commandName === $nameOrAlias || !\in_array($commandName, $commands); + return $commandName === $nameOrAlias || !\in_array($commandName, $commands, true); })); } @@ -803,7 +806,7 @@ public function find(string $name) * * @return Command[] */ - public function all(?string $namespace = null) + public function all(?string $namespace = null): array { $this->init(); @@ -944,56 +947,34 @@ protected function doRenderThrowable(\Throwable $e, OutputInterface $output): vo /** * Configures the input and output instances based on the user arguments and options. - * - * @return void */ - protected function configureIO(InputInterface $input, OutputInterface $output) + protected function configureIO(InputInterface $input, OutputInterface $output): void { - if (true === $input->hasParameterOption(['--ansi'], true)) { + if ($input->hasParameterOption(['--ansi'], true)) { $output->setDecorated(true); - } elseif (true === $input->hasParameterOption(['--no-ansi'], true)) { + } elseif ($input->hasParameterOption(['--no-ansi'], true)) { $output->setDecorated(false); } - if (true === $input->hasParameterOption(['--no-interaction', '-n'], true)) { - $input->setInteractive(false); - } - - switch ($shellVerbosity = (int) getenv('SHELL_VERBOSITY')) { - case -1: - $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); - break; - case 1: - $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); - break; - case 2: - $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); - break; - case 3: - $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); - break; - default: - $shellVerbosity = 0; - break; - } + $shellVerbosity = match (true) { + $input->hasParameterOption(['--silent'], true) => -2, + $input->hasParameterOption(['--quiet', '-q'], true) => -1, + $input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true) => 3, + $input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true) => 2, + $input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true) => 1, + default => (int) ($_ENV['SHELL_VERBOSITY'] ?? $_SERVER['SHELL_VERBOSITY'] ?? getenv('SHELL_VERBOSITY')), + }; - if (true === $input->hasParameterOption(['--quiet', '-q'], true)) { - $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); - $shellVerbosity = -1; - } else { - if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) { - $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); - $shellVerbosity = 3; - } elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) { - $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); - $shellVerbosity = 2; - } elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) { - $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); - $shellVerbosity = 1; - } - } + $output->setVerbosity(match ($shellVerbosity) { + -2 => OutputInterface::VERBOSITY_SILENT, + -1 => OutputInterface::VERBOSITY_QUIET, + 1 => OutputInterface::VERBOSITY_VERBOSE, + 2 => OutputInterface::VERBOSITY_VERY_VERBOSE, + 3 => OutputInterface::VERBOSITY_DEBUG, + default => ($shellVerbosity = 0) ?: $output->getVerbosity(), + }); - if (-1 === $shellVerbosity) { + if (0 > $shellVerbosity || $input->hasParameterOption(['--no-interaction', '-n'], true)) { $input->setInteractive(false); } @@ -1012,7 +993,7 @@ protected function configureIO(InputInterface $input, OutputInterface $output) * * @return int 0 if everything went fine, or an error code */ - protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) + protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output): int { foreach ($command->getHelperSet() as $helper) { if ($helper instanceof InputAwareInterface) { @@ -1021,11 +1002,8 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI } $registeredSignals = false; - $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(); $registeredSignals = true; $this->getSignalRegistry()->pushCurrentHandlers(); @@ -1033,24 +1011,34 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI 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); - // BC layer for Symfony <= 5 - if (null === $exitCode) { - trigger_deprecation('symfony/console', '6.3', 'Not returning an exit code from "%s::handleSignal()" is deprecated, return "false" to keep the command running or "0" to exit successfully.', get_debug_type($command)); - $exitCode = 0; - } + } + + 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()); @@ -1063,15 +1051,12 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI } foreach ($commandSignals as $signal) { - $this->signalRegistry->register($signal, function (int $signal) use ($command): void { - $exitCode = $command->handleSignal($signal); - // BC layer for Symfony <= 5 - if (null === $exitCode) { - trigger_deprecation('symfony/console', '6.3', 'Not returning an exit code from "%s::handleSignal()" is deprecated, return "false" to keep the command running or "0" to exit successfully.', get_debug_type($command)); - $exitCode = 0; + $signalRegistry->register($signal, function (int $signal) use ($command): void { + if (\SIGALRM === $signal) { + $this->scheduleAlarm(); } - if (false !== $exitCode) { + if (false !== $exitCode = $command->handleSignal($signal)) { exit($exitCode); } }); @@ -1147,7 +1132,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), 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 9ccb41d94..9f3ae3d7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,44 @@ 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 +--- + + * Add method `__toString()` to `InputInterface` + * Remove `Command::$defaultName` and `Command::$defaultDescription`, use the `AsCommand` attribute instead + * Require explicit argument when calling `*Command::setApplication()`, `*FormatterStyle::setForeground/setBackground()`, `Helper::setHelpSet()`, `Input*::setDefault()` and `Question::setAutocompleterCallback/setValidator()` + * Remove `StringInput::REGEX_STRING` + 6.4 --- diff --git a/CI/GithubActionReporter.php b/CI/GithubActionReporter.php index 28112c2a2..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 diff --git a/Command/Command.php b/Command/Command.php index 3ede6ca6b..72a10cf76 100644 --- a/Command/Command.php +++ b/Command/Command.php @@ -32,27 +32,13 @@ * * @author Fabien Potencier */ -class Command +class Command implements SignalableCommandInterface { // see https://tldp.org/LDP/abs/html/exitcodes.html public const SUCCESS = 0; public const FAILURE = 1; public const INVALID = 2; - /** - * @var string|null The default command name - * - * @deprecated since Symfony 6.1, use the AsCommand attribute instead - */ - protected static $defaultName; - - /** - * @var string|null The default command description - * - * @deprecated since Symfony 6.1, use the AsCommand attribute instead - */ - protected static $defaultDescription; - private ?Application $application = null; private ?string $name = null; private ?string $processTitle = null; @@ -63,47 +49,37 @@ 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 { - $class = static::class; + 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($class))->getAttributes(AsCommand::class)) { + if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) { return $attribute[0]->newInstance()->name; } - $r = new \ReflectionProperty($class, 'defaultName'); - - if ($class !== $r->class || null === static::$defaultName) { - return null; - } - - trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultName" for setting a command name is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class); - - return static::$defaultName; + return null; } + /** + * @deprecated since Symfony 7.3, use the #[AsCommand] attribute instead + */ public static function getDefaultDescription(): ?string { - $class = static::class; + 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($class))->getAttributes(AsCommand::class)) { + if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) { return $attribute[0]->newInstance()->description; } - $r = new \ReflectionProperty($class, 'defaultDescription'); - - if ($class !== $r->class || null === static::$defaultDescription) { - return null; - } - - trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultDescription" for setting a command description is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class); - - return static::$defaultDescription; + return null; } /** @@ -115,7 +91,19 @@ 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)) { @@ -131,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(); @@ -141,22 +145,14 @@ public function __construct(?string $name = null) * Ignores validation errors. * * This is mainly useful for the help command. - * - * @return void */ - public function ignoreValidationErrors() + public function ignoreValidationErrors(): void { $this->ignoreValidationErrors = true; } - /** - * @return void - */ - public function setApplication(?Application $application = null) + public function setApplication(?Application $application): void { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } $this->application = $application; if ($application) { $this->setHelperSet($application->getHelperSet()); @@ -167,10 +163,7 @@ public function setApplication(?Application $application = null) $this->fullDefinition = null; } - /** - * @return void - */ - public function setHelperSet(HelperSet $helperSet) + public function setHelperSet(HelperSet $helperSet): void { $this->helperSet = $helperSet; } @@ -196,10 +189,8 @@ public function getApplication(): ?Application * * Override this to check for x or y and return false if the command cannot * run properly under the current conditions. - * - * @return bool */ - public function isEnabled() + public function isEnabled(): bool { return true; } @@ -227,7 +218,7 @@ protected function configure() * * @see setCode() */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { throw new LogicException('You must override the execute() method in the concrete command class.'); } @@ -321,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 { @@ -362,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; } @@ -446,26 +415,28 @@ 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 = null */): static + public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static { - $suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : []; - if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) { - throw new \TypeError(\sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues))); - } $this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues)); $this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues)); @@ -475,21 +446,17 @@ 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|null $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 { - $suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : []; - if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) { - throw new \TypeError(\sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues))); - } $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues)); $this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues)); @@ -695,12 +662,10 @@ public function getUsages(): array /** * Gets a helper instance by name. * - * @return HelperInterface - * * @throws LogicException if no HelperSet is defined * @throws InvalidArgumentException if the helper is not defined */ - public function getHelper(string $name): mixed + 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)); @@ -709,6 +674,16 @@ public function getHelper(string $name): mixed 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. * diff --git a/Command/CompleteCommand.php b/Command/CompleteCommand.php index 33f7f93c8..15eeea16a 100644 --- a/Command/CompleteCommand.php +++ b/Command/CompleteCommand.php @@ -34,18 +34,7 @@ final class CompleteCommand extends Command { public const COMPLETION_API_VERSION = '1'; - /** - * @deprecated since Symfony 6.1 - */ - protected static $defaultName = '|_complete'; - - /** - * @deprecated since Symfony 6.1 - */ - protected static $defaultDescription = 'Internal command to provide shell completion suggestions'; - private array $completionOutputs; - private bool $isDebug = false; /** @@ -109,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.'); @@ -196,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 571425b88..2853fc5f4 100644 --- a/Command/DumpCompletionCommand.php +++ b/Command/DumpCompletionCommand.php @@ -27,16 +27,6 @@ #[AsCommand(name: 'completion', description: 'Dump the shell completion script')] final class DumpCompletionCommand extends Command { - /** - * @deprecated since Symfony 6.1 - */ - protected static $defaultName = 'completion'; - - /** - * @deprecated since Symfony 6.1 - */ - protected static $defaultDescription = 'Dump the shell completion script'; - private array $supportedShells; protected function configure(): void @@ -45,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], diff --git a/Command/HelpCommand.php b/Command/HelpCommand.php index e6447b050..a2a72dab4 100644 --- a/Command/HelpCommand.php +++ b/Command/HelpCommand.php @@ -27,10 +27,7 @@ class HelpCommand extends Command { private Command $command; - /** - * @return void - */ - protected function configure() + protected function configure(): void { $this->ignoreValidationErrors(); @@ -57,10 +54,7 @@ protected function configure() ; } - /** - * @return void - */ - public function setCommand(Command $command) + public function setCommand(Command $command): void { $this->command = $command; } 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 b94da6665..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 @@ -45,11 +49,8 @@ public function ignoreValidationErrors(): void $this->getCommand()->ignoreValidationErrors(); } - public function setApplication(?Application $application = null): void + public function setApplication(?Application $application): void { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } if ($this->command instanceof parent) { $this->command->setApplication($application); } @@ -116,9 +117,8 @@ 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 { - $suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : []; $this->getCommand()->addArgument($name, $mode, $description, $default, $suggestedValues); return $this; @@ -127,9 +127,8 @@ 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|null $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 { - $suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : []; $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues); return $this; diff --git a/Command/ListCommand.php b/Command/ListCommand.php index 5850c3d7b..61b4b1b3e 100644 --- a/Command/ListCommand.php +++ b/Command/ListCommand.php @@ -25,10 +25,7 @@ */ class ListCommand extends Command { - /** - * @return void - */ - protected function configure() + protected function configure(): void { $this ->setName('list') diff --git a/Command/LockableTrait.php b/Command/LockableTrait.php index cd7548f02..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,6 +27,8 @@ trait LockableTrait { private ?LockInterface $lock = null; + private ?LockFactory $lockFactory = null; + /** * Locks a command. */ @@ -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/SignalableCommandInterface.php b/Command/SignalableCommandInterface.php index 74d59b086..40b301d18 100644 --- a/Command/SignalableCommandInterface.php +++ b/Command/SignalableCommandInterface.php @@ -26,9 +26,7 @@ public function getSubscribedSignals(): array; /** * The method will be called when the application is signaled. * - * @param int|false $previousExitCode - * * @return int|false The exit code to return or false to continue the normal execution */ - public function handleSignal(int $signal/* , int|false $previousExitCode = 0 */); + public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false; } diff --git a/Command/TraceableCommand.php b/Command/TraceableCommand.php index 9df467b0d..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); @@ -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 { diff --git a/CommandLoader/ContainerCommandLoader.php b/CommandLoader/ContainerCommandLoader.php index 1638f2fd0..eb4945135 100644 --- a/CommandLoader/ContainerCommandLoader.php +++ b/CommandLoader/ContainerCommandLoader.php @@ -22,16 +22,13 @@ */ 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 diff --git a/CommandLoader/FactoryCommandLoader.php b/CommandLoader/FactoryCommandLoader.php index ffe1b520c..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 diff --git a/Completion/CompletionInput.php b/Completion/CompletionInput.php index 79c2f659a..9f9619e18 100644 --- a/Completion/CompletionInput.php +++ b/Completion/CompletionInput.php @@ -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/Cursor.php b/Cursor.php index 45243c796..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+')); } diff --git a/DataCollector/CommandDataCollector.php b/DataCollector/CommandDataCollector.php index 3cbe72b59..724af54a0 100644 --- a/DataCollector/CommandDataCollector.php +++ b/DataCollector/CommandDataCollector.php @@ -37,12 +37,13 @@ public function collect(Request $request, Response $response, ?\Throwable $excep $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, 'max_memory_usage' => $command->maxMemoryUsage, 'verbosity_level' => match ($command->output->getVerbosity()) { + OutputInterface::VERBOSITY_SILENT => 'silent', OutputInterface::VERBOSITY_QUIET => 'quiet', OutputInterface::VERBOSITY_NORMAL => 'normal', OutputInterface::VERBOSITY_VERBOSE => 'verbose', @@ -95,6 +96,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'); diff --git a/DependencyInjection/AddConsoleCommandPass.php b/DependencyInjection/AddConsoleCommandPass.php index 3cf05734b..c9efdf344 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; @@ -29,10 +30,7 @@ */ class AddConsoleCommandPass implements CompilerPassInterface { - /** - * @return void - */ - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { $commandServices = $container->findTaggedServiceIds('console.command', true); $lazyCommandMap = []; @@ -41,22 +39,39 @@ public function process(ContainerBuilder $container) 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) { @@ -75,6 +90,7 @@ public function process(ContainerBuilder $container) } $description = $tags[0]['description'] ?? null; + $help = $tags[0]['help'] ?? null; unset($tags[0]); $lazyCommandMap[$commandName] = $id; @@ -91,6 +107,7 @@ public function process(ContainerBuilder $container) } $description ??= $tag['description'] ?? null; + $help ??= $tag['help'] ?? null; } $definition->addMethodCall('setName', [$commandName]); @@ -103,21 +120,26 @@ public function process(ContainerBuilder $container) $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]); + $escapedDescription = str_replace('%', '%%', $description); + $definition->addMethodCall('setDescription', [$escapedDescription]); $container->register('.'.$id.'.lazy', LazyCommand::class) - ->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]); + ->setArguments([$commandName, $aliases, $escapedDescription, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]); $lazyCommandRefs[$id] = new Reference('.'.$id.'.lazy'); } diff --git a/Descriptor/ApplicationDescription.php b/Descriptor/ApplicationDescription.php index 3f38379c9..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 diff --git a/Descriptor/DescriptorInterface.php b/Descriptor/DescriptorInterface.php index ab468a256..04e5a7c86 100644 --- a/Descriptor/DescriptorInterface.php +++ b/Descriptor/DescriptorInterface.php @@ -20,8 +20,5 @@ */ interface DescriptorInterface { - /** - * @return void - */ - public function describe(OutputInterface $output, object $object, array $options = []); + public function describe(OutputInterface $output, object $object, array $options = []): void; } diff --git a/Descriptor/ReStructuredTextDescriptor.php b/Descriptor/ReStructuredTextDescriptor.php index a2b754276..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); @@ -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/XmlDescriptor.php b/Descriptor/XmlDescriptor.php index 866c71856..00055557c 100644 --- a/Descriptor/XmlDescriptor.php +++ b/Descriptor/XmlDescriptor.php @@ -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 7be2ff83e..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 6ba1615fe..2f9f0778e 100644 --- a/Event/ConsoleEvent.php +++ b/Event/ConsoleEvent.php @@ -23,16 +23,11 @@ */ class ConsoleEvent extends Event { - protected $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 c9ec24434..9acb0e41d 100644 --- a/EventListener/ErrorListener.php +++ b/EventListener/ErrorListener.php @@ -24,17 +24,12 @@ */ class ErrorListener implements EventSubscriberInterface { - private ?LoggerInterface $logger; - - public function __construct(?LoggerInterface $logger = null) - { - $this->logger = $logger; + public function __construct( + private ?LoggerInterface $logger = null, + ) { } - /** - * @return void - */ - public function onConsoleError(ConsoleErrorEvent $event) + public function onConsoleError(ConsoleErrorEvent $event): void { if (null === $this->logger) { return; @@ -42,7 +37,7 @@ public function onConsoleError(ConsoleErrorEvent $event) $error = $event->getError(); - if (!$inputString = $this->getInputString($event)) { + if (!$inputString = self::getInputString($event)) { $this->logger->critical('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]); return; @@ -51,10 +46,7 @@ public function onConsoleError(ConsoleErrorEvent $event) $this->logger->critical('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]); } - /** - * @return void - */ - public function onConsoleTerminate(ConsoleTerminateEvent $event) + public function onConsoleTerminate(ConsoleTerminateEvent $event): void { if (null === $this->logger) { return; @@ -66,7 +58,7 @@ public function onConsoleTerminate(ConsoleTerminateEvent $event) return; } - if (!$inputString = $this->getInputString($event)) { + if (!$inputString = self::getInputString($event)) { $this->logger->debug('The console exited with code "{code}"', ['code' => $exitCode]); return; @@ -83,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 541b32b23..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/NullOutputFormatterStyle.php b/Formatter/NullOutputFormatterStyle.php index ae23decb1..06fa6e40b 100644 --- a/Formatter/NullOutputFormatterStyle.php +++ b/Formatter/NullOutputFormatterStyle.php @@ -21,19 +21,13 @@ public function apply(string $text): string return $text; } - public function setBackground(?string $color = null): void + public function setBackground(?string $color): void { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } // do nothing } - public function setForeground(?string $color = null): void + public function setForeground(?string $color): void { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } // do nothing } diff --git a/Formatter/OutputFormatter.php b/Formatter/OutputFormatter.php index a30e44d84..c72728b27 100644 --- a/Formatter/OutputFormatter.php +++ b/Formatter/OutputFormatter.php @@ -24,7 +24,6 @@ */ class OutputFormatter implements WrappableOutputFormatterInterface { - private bool $decorated; private array $styles = []; private OutputFormatterStyleStack $styleStack; @@ -68,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')); @@ -84,10 +83,7 @@ public function __construct(bool $decorated = false, array $styles = []) $this->styleStack = new OutputFormatterStyleStack(); } - /** - * @return void - */ - public function setDecorated(bool $decorated) + public function setDecorated(bool $decorated): void { $this->decorated = $decorated; } @@ -97,10 +93,7 @@ public function isDecorated(): bool return $this->decorated; } - /** - * @return void - */ - public function setStyle(string $name, OutputFormatterStyleInterface $style) + public function setStyle(string $name, OutputFormatterStyleInterface $style): void { $this->styles[strtolower($name)] = $style; } @@ -124,10 +117,7 @@ public function format(?string $message): ?string return $this->formatAndWrap($message, 0); } - /** - * @return string - */ - public function formatAndWrap(?string $message, int $width) + public function formatAndWrap(?string $message, int $width): string { if (null === $message) { return ''; diff --git a/Formatter/OutputFormatterInterface.php b/Formatter/OutputFormatterInterface.php index 433cd4197..947347fa7 100644 --- a/Formatter/OutputFormatterInterface.php +++ b/Formatter/OutputFormatterInterface.php @@ -20,10 +20,8 @@ interface OutputFormatterInterface { /** * Sets the decorated flag. - * - * @return void */ - public function setDecorated(bool $decorated); + public function setDecorated(bool $decorated): void; /** * Whether the output will decorate messages. @@ -32,10 +30,8 @@ public function isDecorated(): bool; /** * Sets a new style. - * - * @return void */ - public function setStyle(string $name, OutputFormatterStyleInterface $style); + public function setStyle(string $name, OutputFormatterStyleInterface $style): void; /** * Checks if output formatter has style with specified name. diff --git a/Formatter/OutputFormatterStyle.php b/Formatter/OutputFormatterStyle.php index 21e7f5ab0..20a65b517 100644 --- a/Formatter/OutputFormatterStyle.php +++ b/Formatter/OutputFormatterStyle.php @@ -38,25 +38,13 @@ public function __construct(?string $foreground = null, ?string $background = nu $this->color = new Color($this->foreground = $foreground ?: '', $this->background = $background ?: '', $this->options = $options); } - /** - * @return void - */ - public function setForeground(?string $color = null) + public function setForeground(?string $color): void { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } $this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options); } - /** - * @return void - */ - public function setBackground(?string $color = null) + public function setBackground(?string $color): void { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } $this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options); } @@ -65,19 +53,13 @@ public function setHref(string $url): void $this->href = $url; } - /** - * @return void - */ - public function setOption(string $option) + public function setOption(string $option): void { $this->options[] = $option; $this->color = new Color($this->foreground, $this->background, $this->options); } - /** - * @return void - */ - public function unsetOption(string $option) + public function unsetOption(string $option): void { $pos = array_search($option, $this->options); if (false !== $pos) { @@ -87,10 +69,7 @@ public function unsetOption(string $option) $this->color = new Color($this->foreground, $this->background, $this->options); } - /** - * @return void - */ - public function setOptions(array $options) + public function setOptions(array $options): void { $this->color = new Color($this->foreground, $this->background, $this->options = $options); } diff --git a/Formatter/OutputFormatterStyleInterface.php b/Formatter/OutputFormatterStyleInterface.php index 3b15098cb..037419277 100644 --- a/Formatter/OutputFormatterStyleInterface.php +++ b/Formatter/OutputFormatterStyleInterface.php @@ -20,38 +20,28 @@ interface OutputFormatterStyleInterface { /** * Sets style foreground color. - * - * @return void */ - public function setForeground(?string $color); + public function setForeground(?string $color): void; /** * Sets style background color. - * - * @return void */ - public function setBackground(?string $color); + public function setBackground(?string $color): void; /** * Sets some specific style option. - * - * @return void */ - public function setOption(string $option); + public function setOption(string $option): void; /** * Unsets some specific style option. - * - * @return void */ - public function unsetOption(string $option); + public function unsetOption(string $option): void; /** * Sets multiple style options at once. - * - * @return void */ - public function setOptions(array $options); + public function setOptions(array $options): void; /** * Applies the style to a given text. diff --git a/Formatter/OutputFormatterStyleStack.php b/Formatter/OutputFormatterStyleStack.php index 62d2ca0e7..4985213ab 100644 --- a/Formatter/OutputFormatterStyleStack.php +++ b/Formatter/OutputFormatterStyleStack.php @@ -34,20 +34,16 @@ public function __construct(?OutputFormatterStyleInterface $emptyStyle = null) /** * Resets stack (ie. empty internal arrays). - * - * @return void */ - public function reset() + public function reset(): void { $this->styles = []; } /** * Pushes a style in the stack. - * - * @return void */ - public function push(OutputFormatterStyleInterface $style) + public function push(OutputFormatterStyleInterface $style): void { $this->styles[] = $style; } diff --git a/Formatter/WrappableOutputFormatterInterface.php b/Formatter/WrappableOutputFormatterInterface.php index 746cd27e7..412d9976f 100644 --- a/Formatter/WrappableOutputFormatterInterface.php +++ b/Formatter/WrappableOutputFormatterInterface.php @@ -20,8 +20,6 @@ interface WrappableOutputFormatterInterface extends OutputFormatterInterface { /** * Formats a message according to the given styles, wrapping at `$width` (0 means no wrapping). - * - * @return string */ - public function formatAndWrap(?string $message, int $width); + public function formatAndWrap(?string $message, int $width): string; } diff --git a/Helper/DescriptorHelper.php b/Helper/DescriptorHelper.php index fda6779b9..9422271fb 100644 --- a/Helper/DescriptorHelper.php +++ b/Helper/DescriptorHelper.php @@ -50,11 +50,9 @@ public function __construct() * * format: string, the output format name * * raw_text: boolean, sets output type as raw * - * @return void - * * @throws InvalidArgumentException when the given format is not supported */ - public function describe(OutputInterface $output, ?object $object, array $options = []) + public function describe(OutputInterface $output, ?object $object, array $options = []): void { $options = array_merge([ 'raw_text' => false, diff --git a/Helper/Dumper.php b/Helper/Dumper.php index a3b8e3952..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/Helper.php b/Helper/Helper.php index 468d06689..46e7e2f58 100644 --- a/Helper/Helper.php +++ b/Helper/Helper.php @@ -21,16 +21,10 @@ */ abstract class Helper implements HelperInterface { - protected $helperSet; + protected ?HelperSet $helperSet = null; - /** - * @return void - */ - public function setHelperSet(?HelperSet $helperSet = null) + public function setHelperSet(?HelperSet $helperSet): void { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } $this->helperSet = $helperSet; } @@ -97,53 +91,49 @@ public static function substr(?string $string, int $from, ?int $length = null): return mb_substr($string, $from, $length, $encoding); } - /** - * @return string - */ - public static function formatTime(int|float $secs, int $precision = 1) + 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)); } - /** - * @return string - */ - public static function formatMemory(int $memory) + public static function formatMemory(int $memory): string { if ($memory >= 1024 * 1024 * 1024) { return \sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024); @@ -160,10 +150,7 @@ public static function formatMemory(int $memory) return \sprintf('%d B', $memory); } - /** - * @return string - */ - public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string) + public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string): string { $isDecorated = $formatter->isDecorated(); $formatter->setDecorated(false); diff --git a/Helper/HelperInterface.php b/Helper/HelperInterface.php index ab626c938..8c4da3c91 100644 --- a/Helper/HelperInterface.php +++ b/Helper/HelperInterface.php @@ -20,10 +20,8 @@ interface HelperInterface { /** * Sets the helper set associated with this helper. - * - * @return void */ - public function setHelperSet(?HelperSet $helperSet); + public function setHelperSet(?HelperSet $helperSet): void; /** * Gets the helper set associated with this helper. @@ -32,8 +30,6 @@ public function getHelperSet(): ?HelperSet; /** * Returns the canonical name of this helper. - * - * @return string */ - public function getName(); + public function getName(): string; } diff --git a/Helper/HelperSet.php b/Helper/HelperSet.php index 8deb22ee7..ffe756c9d 100644 --- a/Helper/HelperSet.php +++ b/Helper/HelperSet.php @@ -35,10 +35,7 @@ public function __construct(array $helpers = []) } } - /** - * @return void - */ - public function set(HelperInterface $helper, ?string $alias = null) + public function set(HelperInterface $helper, ?string $alias = null): void { $this->helpers[$helper->getName()] = $helper; if (null !== $alias) { diff --git a/Helper/InputAwareHelper.php b/Helper/InputAwareHelper.php index 6f8225973..47126bdaa 100644 --- a/Helper/InputAwareHelper.php +++ b/Helper/InputAwareHelper.php @@ -21,12 +21,9 @@ */ abstract class InputAwareHelper extends Helper implements InputAwareInterface { - protected $input; + protected InputInterface $input; - /** - * @return void - */ - public function setInput(InputInterface $input) + public function setInput(InputInterface $input): void { $this->input = $input; } diff --git a/Helper/ProcessHelper.php b/Helper/ProcessHelper.php index ae55a83c2..4a8cfc9d9 100644 --- a/Helper/ProcessHelper.php +++ b/Helper/ProcessHelper.php @@ -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); diff --git a/Helper/ProgressBar.php b/Helper/ProgressBar.php index 8143acaff..dc3605ad2 100644 --- a/Helper/ProgressBar.php +++ b/Helper/ProgressBar.php @@ -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 @@ -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 @@ -315,7 +315,21 @@ public function maxSecondsBetweenRedraws(float $seconds): void */ 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; @@ -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); } /** @@ -551,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.'); } @@ -592,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); diff --git a/Helper/ProgressIndicator.php b/Helper/ProgressIndicator.php index 92106caf6..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,30 +50,32 @@ 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(); } /** * Sets the current indicator message. - * - * @return void */ - public function setMessage(?string $message) + public function setMessage(?string $message): void { $this->message = $message; @@ -82,10 +84,8 @@ public function setMessage(?string $message) /** * Starts the indicator output. - * - * @return void */ - public function start(string $message) + public function start(string $message): void { if ($this->started) { throw new LogicException('Progress indicator already started.'); @@ -93,6 +93,7 @@ public function start(string $message) $this->message = $message; $this->started = true; + $this->finished = false; $this->startTime = time(); $this->indicatorUpdateTime = $this->getCurrentTimeInMilliseconds() + $this->indicatorChangeInterval; $this->indicatorCurrent = 0; @@ -102,10 +103,8 @@ public function start(string $message) /** * Advances the indicator. - * - * @return void */ - public function advance() + public function advance(): void { if (!$this->started) { throw new LogicException('Progress indicator has not yet been started.'); @@ -130,14 +129,24 @@ public function advance() /** * Finish the indicator with message. * - * @return void + * @param ?string $finishedIndicator */ - public function finish(string $message) + public function finish(string $message/* , ?string $finishedIndicator = null */): void { + $finishedIndicator = 1 < \func_num_args() ? func_get_arg(1) : null; + if (null !== $finishedIndicator && !\is_string($finishedIndicator)) { + throw new \TypeError(\sprintf('Argument 2 passed to "%s()" must be of the type string or null, "%s" given.', __METHOD__, get_debug_type($finishedIndicator))); + } + if (!$this->started) { throw new LogicException('Progress indicator has not yet been started.'); } + if (null !== $finishedIndicator) { + $this->finishedIndicatorValue = $finishedIndicator; + } + + $this->finished = true; $this->message = $message; $this->display(); $this->output->writeln(''); @@ -156,10 +165,8 @@ public static function getFormatDefinition(string $name): ?string * Sets a placeholder formatter for a given name. * * This method also allow you to override an existing placeholder. - * - * @return void */ - public static function setPlaceholderFormatterDefinition(string $name, callable $callable) + public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void { self::$formatters ??= self::initPlaceholderFormatters(); @@ -182,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); } @@ -226,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 23d8522ad..3381be1a9 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) { @@ -89,10 +83,8 @@ public function getName(): string /** * Prevents usage of stty. - * - * @return void */ - public static function disableStty() + public static function disableStty(): void { self::$stty = false; } @@ -100,13 +92,14 @@ public static function disableStty() /** * 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()) { @@ -190,10 +183,8 @@ private function getDefaultAnswer(Question $question): mixed /** * Outputs the question prompt. - * - * @return void */ - protected function writePrompt(OutputInterface $output, Question $question) + protected function writePrompt(OutputInterface $output, Question $question): void { $message = $question->getQuestion(); @@ -228,10 +219,8 @@ protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string /** * Outputs an error message. - * - * @return void */ - protected function writeError(OutputInterface $output, \Exception $error) + protected function writeError(OutputInterface $output, \Exception $error): void { if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) { $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'); diff --git a/Helper/SymfonyQuestionHelper.php b/Helper/SymfonyQuestionHelper.php index 11b4e4238..b452bf047 100644 --- a/Helper/SymfonyQuestionHelper.php +++ b/Helper/SymfonyQuestionHelper.php @@ -25,10 +25,7 @@ */ class SymfonyQuestionHelper extends QuestionHelper { - /** - * @return void - */ - protected function writePrompt(OutputInterface $output, Question $question) + protected function writePrompt(OutputInterface $output, Question $question): void { $text = OutputFormatter::escapeTrailingBackslash($question->getQuestion()); $default = $question->getDefault(); @@ -83,10 +80,7 @@ protected function writePrompt(OutputInterface $output, Question $question) $output->write($prompt); } - /** - * @return void - */ - protected function writeError(OutputInterface $output, \Exception $error) + protected function writeError(OutputInterface $output, \Exception $error): void { if ($output instanceof SymfonyStyle) { $output->newLine(); diff --git a/Helper/Table.php b/Helper/Table.php index 469d228d6..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'); @@ -66,10 +64,8 @@ public function __construct(OutputInterface $output) /** * Sets a style definition. - * - * @return void */ - public static function setStyleDefinition(string $name, TableStyle $style) + public static function setStyleDefinition(string $name, TableStyle $style): void { self::$styles ??= self::initStyles(); @@ -194,7 +190,7 @@ public function setHeaders(array $headers): static /** * @return $this */ - public function setRows(array $rows) + public function setRows(array $rows): static { $this->rows = []; @@ -312,10 +308,8 @@ public function setVertical(bool $vertical = true): static * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | * | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | * +---------------+-----------------------+------------------+ - * - * @return void */ - public function render() + public function render(): void { $divider = new TableSeparator(); $isCellWithColspan = static fn ($cell) => $cell instanceof TableCell && $cell->getColspan() >= 2; @@ -423,7 +417,7 @@ public function render() continue; } - if ($isHeader && !$isHeaderSeparatorRendered) { + if ($isHeader && !$isHeaderSeparatorRendered && $this->style->displayOutsideBorder()) { $this->renderRowSeparator( self::SEPARATOR_TOP, $hasTitle ? $this->headerTitle : null, @@ -455,7 +449,10 @@ public function render() } } } - $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; @@ -762,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]); @@ -770,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]); @@ -911,6 +908,12 @@ private function cleanup(): void */ private static function initStyles(): array { + $markdown = new TableStyle(); + $markdown + ->setDefaultCrossingChar('|') + ->setDisplayOutsideBorder(false) + ; + $borderless = new TableStyle(); $borderless ->setHorizontalBorderChars('=') @@ -948,6 +951,7 @@ private static function initStyles(): array return [ 'default' => new TableStyle(), + 'markdown' => $markdown, 'borderless' => $borderless, 'compact' => $compact, 'symfony-style-guide' => $styleGuide, diff --git a/Helper/TableCell.php b/Helper/TableCell.php index ead32f283..ab8339204 100644 --- a/Helper/TableCell.php +++ b/Helper/TableCell.php @@ -18,17 +18,16 @@ */ 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))); diff --git a/Helper/TableCellStyle.php b/Helper/TableCellStyle.php index 1b1ef276e..af1a17e96 100644 --- a/Helper/TableCellStyle.php +++ b/Helper/TableCellStyle.php @@ -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 be956c109..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; /** @@ -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 b5f866689..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; + /** @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,18 +63,13 @@ public function __construct(?array $argv = null, ?InputDefinition $definition = parent::__construct($definition); } - /** - * @return void - */ - protected function setTokens(array $tokens) + /** @param list $tokens */ + protected function setTokens(array $tokens): void { $this->tokens = $tokens; } - /** - * @return void - */ - protected function parse() + protected function parse(): void { $parseOptions = true; $this->parsed = $this->tokens; @@ -130,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); } } @@ -348,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 b9f753394..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); } @@ -113,10 +111,7 @@ public function __toString(): string return implode(' ', $params); } - /** - * @return void - */ - protected function parse() + protected function parse(): void { foreach ($this->parameters as $key => $value) { if ('--' === $key) { diff --git a/Input/Input.php b/Input/Input.php index d3a3c7fd2..d2881c60f 100644 --- a/Input/Input.php +++ b/Input/Input.php @@ -27,12 +27,12 @@ */ abstract class Input implements InputInterface, StreamableInputInterface { - protected $definition; + protected InputDefinition $definition; /** @var resource */ protected $stream; - protected $options = []; - protected $arguments = []; - protected $interactive = true; + protected array $options = []; + protected array $arguments = []; + protected bool $interactive = true; public function __construct(?InputDefinition $definition = null) { @@ -44,10 +44,7 @@ public function __construct(?InputDefinition $definition = null) } } - /** - * @return void - */ - public function bind(InputDefinition $definition) + public function bind(InputDefinition $definition): void { $this->arguments = []; $this->options = []; @@ -58,15 +55,10 @@ public function bind(InputDefinition $definition) /** * Processes command line arguments. - * - * @return void */ - abstract protected function parse(); + abstract protected function parse(): void; - /** - * @return void - */ - public function validate() + public function validate(): void { $definition = $this->definition; $givenArguments = $this->arguments; @@ -83,10 +75,7 @@ public function isInteractive(): bool return $this->interactive; } - /** - * @return void - */ - public function setInteractive(bool $interactive) + public function setInteractive(bool $interactive): void { $this->interactive = $interactive; } @@ -105,10 +94,7 @@ public function getArgument(string $name): mixed return $this->arguments[$name] ?? $this->definition->getArgument($name)->getDefault(); } - /** - * @return void - */ - public function setArgument(string $name, mixed $value) + public function setArgument(string $name, mixed $value): void { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(\sprintf('The "%s" argument does not exist.', $name)); @@ -144,10 +130,7 @@ public function getOption(string $name): mixed return \array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); } - /** - * @return void - */ - public function setOption(string $name, mixed $value) + public function setOption(string $name, mixed $value): void { if ($this->definition->hasNegation($name)) { $this->options[$this->definition->negationToName($name)] = !$value; @@ -175,10 +158,8 @@ public function escapeToken(string $token): string /** * @param resource $stream - * - * @return void */ - public function setStream($stream) + public function setStream($stream): void { $this->stream = $stream; } diff --git a/Input/InputArgument.php b/Input/InputArgument.php index fd203919f..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|float|null $default; - private array|\Closure $suggestedValues; - private string $description; /** * @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|null $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) { + } 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,16 +100,9 @@ public function isArray(): bool /** * Sets the default value. - * - * @return void - * - * @throws LogicException When incorrect default value is given */ - public function setDefault(string|bool|int|float|array|null $default = null) + public function setDefault(string|bool|int|float|array|null $default): void { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } if ($this->isRequired() && null !== $default) { throw new LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); } @@ -123,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() */ diff --git a/Input/InputAwareInterface.php b/Input/InputAwareInterface.php index 0ad27b455..ba4664cdb 100644 --- a/Input/InputAwareInterface.php +++ b/Input/InputAwareInterface.php @@ -21,8 +21,6 @@ interface InputAwareInterface { /** * Sets the Console Input. - * - * @return void */ - public function setInput(InputInterface $input); + public function setInput(InputInterface $input): void; } diff --git a/Input/InputDefinition.php b/Input/InputDefinition.php index b5c202838..a8b006d48 100644 --- a/Input/InputDefinition.php +++ b/Input/InputDefinition.php @@ -46,10 +46,8 @@ public function __construct(array $definition = []) /** * Sets the definition of the input. - * - * @return void */ - public function setDefinition(array $definition) + public function setDefinition(array $definition): void { $arguments = []; $options = []; @@ -69,10 +67,8 @@ public function setDefinition(array $definition) * Sets the InputArgument objects. * * @param InputArgument[] $arguments An array of InputArgument objects - * - * @return void */ - public function setArguments(array $arguments = []) + public function setArguments(array $arguments = []): void { $this->arguments = []; $this->requiredCount = 0; @@ -85,10 +81,8 @@ public function setArguments(array $arguments = []) * Adds an array of InputArgument objects. * * @param InputArgument[] $arguments An array of InputArgument objects - * - * @return void */ - public function addArguments(?array $arguments = []) + public function addArguments(?array $arguments = []): void { if (null !== $arguments) { foreach ($arguments as $argument) { @@ -98,11 +92,9 @@ public function addArguments(?array $arguments = []) } /** - * @return void - * * @throws LogicException When incorrect argument is given */ - public function addArgument(InputArgument $argument) + public function addArgument(InputArgument $argument): void { if (isset($this->arguments[$argument->getName()])) { throw new LogicException(\sprintf('An argument with name "%s" already exists.', $argument->getName())); @@ -198,10 +190,8 @@ public function getArgumentDefaults(): array * Sets the InputOption objects. * * @param InputOption[] $options An array of InputOption objects - * - * @return void */ - public function setOptions(array $options = []) + public function setOptions(array $options = []): void { $this->options = []; $this->shortcuts = []; @@ -213,10 +203,8 @@ public function setOptions(array $options = []) * Adds an array of InputOption objects. * * @param InputOption[] $options An array of InputOption objects - * - * @return void */ - public function addOptions(array $options = []) + public function addOptions(array $options = []): void { foreach ($options as $option) { $this->addOption($option); @@ -224,11 +212,9 @@ public function addOptions(array $options = []) } /** - * @return void - * * @throws LogicException When option given already exist */ - public function addOption(InputOption $option) + public function addOption(InputOption $option): void { if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) { throw new LogicException(\sprintf('An option named "%s" already exists.', $option->getName())); diff --git a/Input/InputInterface.php b/Input/InputInterface.php index aaed5fd01..c177d960b 100644 --- a/Input/InputInterface.php +++ b/Input/InputInterface.php @@ -18,9 +18,6 @@ * InputInterface is the interface implemented by all input classes. * * @author Fabien Potencier - * - * @method string __toString() Returns a stringified representation of the args passed to the command. - * InputArguments MUST be escaped as well as the InputOption values passed to the command. */ interface InputInterface { @@ -53,28 +50,22 @@ public function hasParameterOption(string|array $values, bool $onlyParams = fals * @param string|array $values The value(s) to look for in the raw parameters (can be an array) * @param string|bool|int|float|array|null $default The default value to return if no result is found * @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal - * - * @return mixed */ - public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false); + public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed; /** * Binds the current Input instance with the given arguments and options. * - * @return void - * * @throws RuntimeException */ - public function bind(InputDefinition $definition); + public function bind(InputDefinition $definition): void; /** * Validates the input. * - * @return void - * * @throws RuntimeException When not enough arguments are given */ - public function validate(); + public function validate(): void; /** * Returns all the given arguments merged with the default values. @@ -86,20 +77,16 @@ public function getArguments(): array; /** * Returns the argument value for a given argument name. * - * @return mixed - * * @throws InvalidArgumentException When argument given doesn't exist */ - public function getArgument(string $name); + public function getArgument(string $name): mixed; /** * Sets an argument value by name. * - * @return void - * * @throws InvalidArgumentException When argument given doesn't exist */ - public function setArgument(string $name, mixed $value); + public function setArgument(string $name, mixed $value): void; /** * Returns true if an InputArgument object exists by name or position. @@ -116,20 +103,16 @@ public function getOptions(): array; /** * Returns the option value for a given option name. * - * @return mixed - * * @throws InvalidArgumentException When option given doesn't exist */ - public function getOption(string $name); + public function getOption(string $name): mixed; /** * Sets an option value by name. * - * @return void - * * @throws InvalidArgumentException When option given doesn't exist */ - public function setOption(string $name, mixed $value); + public function setOption(string $name, mixed $value): void; /** * Returns true if an InputOption object exists by name. @@ -143,8 +126,13 @@ public function isInteractive(): bool; /** * Sets the input interactivity. + */ + public function setInteractive(bool $interactive): void; + + /** + * Returns a stringified representation of the args passed to the command. * - * @return void + * InputArguments MUST be escaped as well as the InputOption values passed to the command. */ - public function setInteractive(bool $interactive); + public function __toString(): string; } diff --git a/Input/InputOption.php b/Input/InputOption.php index 3adc8c424..25fb91782 100644 --- a/Input/InputOption.php +++ b/Input/InputOption.php @@ -46,32 +46,36 @@ class InputOption public const VALUE_IS_ARRAY = 8; /** - * The option may have either positive or negative value (e.g. --ansi or --no-ansi). + * The option allows passing a negated variant (e.g. --ansi or --no-ansi). */ public const VALUE_NEGATABLE = 16; private string $name; - private string|array|null $shortcut; + private ?string $shortcut; private int $mode; private string|int|bool|array|float|null $default; - private array|\Closure $suggestedValues; - private string $description; /** * @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|null $shortcut = null, ?int $mode = null, string $description = '', string|bool|int|float|array|null $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.'); } @@ -101,8 +105,6 @@ public function __construct(string $name, string|array|null $shortcut = null, ?i $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,19 +175,21 @@ 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); } /** - * @return void + * Sets the default value. */ - public function setDefault(string|bool|int|float|array|null $default = null) + public function setDefault(string|bool|int|float|array|null $default): void { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { throw new LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); } @@ -217,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() */ diff --git a/Input/StreamableInputInterface.php b/Input/StreamableInputInterface.php index 4b95fcb11..4a0dc017f 100644 --- a/Input/StreamableInputInterface.php +++ b/Input/StreamableInputInterface.php @@ -25,10 +25,8 @@ interface StreamableInputInterface extends InputInterface * This is mainly useful for testing purpose. * * @param resource $stream The input stream - * - * @return void */ - public function setStream($stream); + public function setStream($stream): void; /** * Returns the input stream. diff --git a/Input/StringInput.php b/Input/StringInput.php index 9b94784af..a70f048f9 100644 --- a/Input/StringInput.php +++ b/Input/StringInput.php @@ -24,10 +24,6 @@ */ class StringInput extends ArgvInput { - /** - * @deprecated since Symfony 6.1 - */ - public const REGEX_STRING = '([^\s]+?)(?:\s|(? + * * @throws InvalidArgumentException When unable to parse input (should never happen) */ private function tokenize(string $input): array diff --git a/Logger/ConsoleLogger.php b/Logger/ConsoleLogger.php index 70432b7de..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; } diff --git a/Messenger/RunCommandMessageHandler.php b/Messenger/RunCommandMessageHandler.php index 3f4286542..df5f48af0 100644 --- a/Messenger/RunCommandMessageHandler.php +++ b/Messenger/RunCommandMessageHandler.php @@ -24,8 +24,9 @@ */ final class RunCommandMessageHandler { - public function __construct(private readonly Application $application) - { + public function __construct( + private readonly Application $application, + ) { } public function __invoke(RunCommandMessage $message): RunCommandContext diff --git a/Output/AnsiColorMode.php b/Output/AnsiColorMode.php index dcdd1d5c4..0e1422a27 100644 --- a/Output/AnsiColorMode.php +++ b/Output/AnsiColorMode.php @@ -62,7 +62,7 @@ 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::Ansi8 => '8;5;'.$this->convertFromRGB($r, $g, $b), self::Ansi24 => \sprintf('8;2;%d;%d;%d', $r, $g, $b), }; } @@ -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/BufferedOutput.php b/Output/BufferedOutput.php index ef5099bfd..3c8d3906f 100644 --- a/Output/BufferedOutput.php +++ b/Output/BufferedOutput.php @@ -29,10 +29,7 @@ public function fetch(): string return $content; } - /** - * @return void - */ - protected function doWrite(string $message, bool $newline) + protected function doWrite(string $message, bool $newline): void { $this->buffer .= $message; diff --git a/Output/ConsoleOutput.php b/Output/ConsoleOutput.php index 5837e74a3..2ad3dbcf3 100644 --- a/Output/ConsoleOutput.php +++ b/Output/ConsoleOutput.php @@ -64,28 +64,19 @@ public function section(): ConsoleSectionOutput return new ConsoleSectionOutput($this->getStream(), $this->consoleSectionOutputs, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter()); } - /** - * @return void - */ - public function setDecorated(bool $decorated) + public function setDecorated(bool $decorated): void { parent::setDecorated($decorated); $this->stderr->setDecorated($decorated); } - /** - * @return void - */ - public function setFormatter(OutputFormatterInterface $formatter) + public function setFormatter(OutputFormatterInterface $formatter): void { parent::setFormatter($formatter); $this->stderr->setFormatter($formatter); } - /** - * @return void - */ - public function setVerbosity(int $level) + public function setVerbosity(int $level): void { parent::setVerbosity($level); $this->stderr->setVerbosity($level); @@ -96,10 +87,7 @@ public function getErrorOutput(): OutputInterface return $this->stderr; } - /** - * @return void - */ - public function setErrorOutput(OutputInterface $error) + public function setErrorOutput(OutputInterface $error): void { $this->stderr = $error; } diff --git a/Output/ConsoleOutputInterface.php b/Output/ConsoleOutputInterface.php index 9c0049c8f..1f8f147ce 100644 --- a/Output/ConsoleOutputInterface.php +++ b/Output/ConsoleOutputInterface.php @@ -24,10 +24,7 @@ interface ConsoleOutputInterface extends OutputInterface */ public function getErrorOutput(): OutputInterface; - /** - * @return void - */ - public function setErrorOutput(OutputInterface $error); + public function setErrorOutput(OutputInterface $error): void; public function section(): ConsoleSectionOutput; } diff --git a/Output/ConsoleSectionOutput.php b/Output/ConsoleSectionOutput.php index 04d587cf2..44728dfd4 100644 --- a/Output/ConsoleSectionOutput.php +++ b/Output/ConsoleSectionOutput.php @@ -60,12 +60,10 @@ public function setMaxHeight(int $maxHeight): void * Clears previous output for this section. * * @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared - * - * @return void */ - public function clear(?int $lines = null) + public function clear(?int $lines = null): void { - if (empty($this->content) || !$this->isDecorated()) { + if (!$this->content || !$this->isDecorated()) { return; } @@ -83,10 +81,8 @@ public function clear(?int $lines = null) /** * Overwrites the previous output with a new message. - * - * @return void */ - public function overwrite(string|iterable $message) + public function overwrite(string|iterable $message): void { $this->clear(); $this->writeln($message); @@ -162,10 +158,7 @@ public function addNewLineOfInputSubmit(): void ++$this->lines; } - /** - * @return void - */ - protected function doWrite(string $message, bool $newline) + protected function doWrite(string $message, bool $newline): void { // Simulate newline behavior for consistent output formatting, avoiding extra logic if (!$newline && str_ends_with($message, \PHP_EOL)) { diff --git a/Output/NullOutput.php b/Output/NullOutput.php index f3aa15b1d..8bec706d4 100644 --- a/Output/NullOutput.php +++ b/Output/NullOutput.php @@ -26,10 +26,7 @@ class NullOutput implements OutputInterface { private NullOutputFormatter $formatter; - /** - * @return void - */ - public function setFormatter(OutputFormatterInterface $formatter) + public function setFormatter(OutputFormatterInterface $formatter): void { // do nothing } @@ -40,10 +37,7 @@ public function getFormatter(): OutputFormatterInterface return $this->formatter ??= new NullOutputFormatter(); } - /** - * @return void - */ - public function setDecorated(bool $decorated) + public function setDecorated(bool $decorated): void { // do nothing } @@ -53,24 +47,26 @@ public function isDecorated(): bool return false; } - /** - * @return void - */ - public function setVerbosity(int $level) + public function setVerbosity(int $level): void { // do nothing } 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; @@ -86,18 +82,12 @@ public function isDebug(): bool return false; } - /** - * @return void - */ - public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL) + public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL): void { // do nothing } - /** - * @return void - */ - public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) + public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL): void { // do nothing } diff --git a/Output/Output.php b/Output/Output.php index 00f481e03..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 */ @@ -44,10 +45,7 @@ public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $deco $this->formatter->setDecorated($decorated); } - /** - * @return void - */ - public function setFormatter(OutputFormatterInterface $formatter) + public function setFormatter(OutputFormatterInterface $formatter): void { $this->formatter = $formatter; } @@ -57,10 +55,7 @@ public function getFormatter(): OutputFormatterInterface return $this->formatter; } - /** - * @return void - */ - public function setDecorated(bool $decorated) + public function setDecorated(bool $decorated): void { $this->formatter->setDecorated($decorated); } @@ -70,10 +65,7 @@ public function isDecorated(): bool return $this->formatter->isDecorated(); } - /** - * @return void - */ - public function setVerbosity(int $level) + public function setVerbosity(int $level): void { $this->verbosity = $level; } @@ -83,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; @@ -103,18 +100,12 @@ public function isDebug(): bool return self::VERBOSITY_DEBUG <= $this->verbosity; } - /** - * @return void - */ - public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL) + public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL): void { $this->write($messages, true, $options); } - /** - * @return void - */ - public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) + public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL): void { if (!is_iterable($messages)) { $messages = [$messages]; @@ -148,8 +139,6 @@ public function write(string|iterable $messages, bool $newline = false, int $opt /** * Writes a message to the output. - * - * @return void */ - abstract protected function doWrite(string $message, bool $newline); + abstract protected function doWrite(string $message, bool $newline): void; } diff --git a/Output/OutputInterface.php b/Output/OutputInterface.php index 19a817901..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; @@ -36,29 +39,23 @@ interface OutputInterface * @param bool $newline Whether to add a newline * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), * 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL - * - * @return void */ - public function write(string|iterable $messages, bool $newline = false, int $options = 0); + public function write(string|iterable $messages, bool $newline = false, int $options = 0): void; /** * Writes a message to the output and adds a newline at the end. * * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), * 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL - * - * @return void */ - public function writeln(string|iterable $messages, int $options = 0); + public function writeln(string|iterable $messages, int $options = 0): void; /** * Sets the verbosity of the output. * * @param self::VERBOSITY_* $level - * - * @return void */ - public function setVerbosity(int $level); + public function setVerbosity(int $level): void; /** * Gets the current verbosity of the output. @@ -89,20 +86,15 @@ public function isDebug(): bool; /** * Sets the decorated flag. - * - * @return void */ - public function setDecorated(bool $decorated); + public function setDecorated(bool $decorated): void; /** * Gets the decorated flag. */ public function isDecorated(): bool; - /** - * @return void - */ - public function setFormatter(OutputFormatterInterface $formatter); + public function setFormatter(OutputFormatterInterface $formatter): void; /** * Returns current output formatter instance. diff --git a/Output/StreamOutput.php b/Output/StreamOutput.php index f51d03763..ce5a825e8 100644 --- a/Output/StreamOutput.php +++ b/Output/StreamOutput.php @@ -63,10 +63,7 @@ public function getStream() return $this->stream; } - /** - * @return void - */ - protected function doWrite(string $message, bool $newline) + protected function doWrite(string $message, bool $newline): void { if ($newline) { $message .= \PHP_EOL; @@ -97,6 +94,11 @@ protected function hasColorSupport(): bool return false; } + // Follow https://force-color.org/ + if ('' !== (($_SERVER['FORCE_COLOR'] ?? getenv('FORCE_COLOR'))[0] ?? '')) { + return true; + } + // Detect msysgit/mingw and assume this is a tty because detection // does not work correctly, see https://github.com/composer/composer/issues/9690 if (!@stream_isatty($this->stream) && !\in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { diff --git a/Output/TrimmedBufferOutput.php b/Output/TrimmedBufferOutput.php index 90ee45aae..33db072c5 100644 --- a/Output/TrimmedBufferOutput.php +++ b/Output/TrimmedBufferOutput.php @@ -45,10 +45,7 @@ public function fetch(): string return $content; } - /** - * @return void - */ - protected function doWrite(string $message, bool $newline) + protected function doWrite(string $message, bool $newline): void { $this->buffer .= $message; @@ -56,6 +53,6 @@ protected function doWrite(string $message, bool $newline) $this->buffer .= \PHP_EOL; } - $this->buffer = substr($this->buffer, 0 - $this->maxLength); + $this->buffer = substr($this->buffer, -$this->maxLength); } } diff --git a/Question/ChoiceQuestion.php b/Question/ChoiceQuestion.php index bcfe4dc45..e34ac5d21 100644 --- a/Question/ChoiceQuestion.php +++ b/Question/ChoiceQuestion.php @@ -20,7 +20,6 @@ */ class ChoiceQuestion extends Question { - private array $choices; private bool $multiselect = false; private string $prompt = ' > '; private string $errorMessage = 'Value "%s" is invalid'; @@ -30,15 +29,17 @@ class ChoiceQuestion extends Question * @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, string|bool|int|float|null $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); } 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 ecbde56d5..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|float|null $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|null $default = null) - { - $this->question = $question; - $this->default = $default; + public function __construct( + private string $question, + private string|bool|int|float|null $default = null, + ) { } /** @@ -175,11 +173,8 @@ public function getAutocompleterCallback(): ?callable * * @return $this */ - public function setAutocompleterCallback(?callable $callback = null): static + public function setAutocompleterCallback(?callable $callback): static { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } if ($this->hidden && null !== $callback) { throw new LogicException('A hidden question cannot use the autocompleter.'); } @@ -194,11 +189,8 @@ public function setAutocompleterCallback(?callable $callback = null): static * * @return $this */ - public function setValidator(?callable $validator = null): static + public function setValidator(?callable $validator): static { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } $this->validator = null === $validator ? null : $validator(...); return $this; @@ -266,10 +258,7 @@ public function getNormalizer(): ?callable return $this->normalizer; } - /** - * @return bool - */ - protected function isAssoc(array $array) + protected function isAssoc(array $array): bool { return (bool) \count(array_filter(array_keys($array), 'is_string')); } diff --git a/README.md b/README.md index e9013182a..92f70e714 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,7 @@ interfaces. Sponsor ------- -The Console component for Symfony 6.4 is [backed][1] by [Les-Tilleuls.coop][2]. - -Les-Tilleuls.coop is a team of 70+ Symfony experts who can help you design, develop and -fix your projects. They provide a wide range of professional services including development, -consulting, coaching, training and audits. They also are highly skilled in JS, Go and DevOps. -They are a worker cooperative! - -Help Symfony by [sponsoring][3] its development! +Help Symfony by [sponsoring][1] its development! Resources --------- @@ -31,6 +24,4 @@ Credits `Resources/bin/hiddeninput.exe` is a third party binary provided within this component. Find sources and license at https://github.com/Seldaek/hidden-input. -[1]: https://symfony.com/backers -[2]: https://les-tilleuls.coop -[3]: https://symfony.com/sponsor +[1]: https://symfony.com/sponsor diff --git a/Resources/completion.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/SignalRegistry.php b/SignalRegistry/SignalRegistry.php index ac8851b06..4019a7f88 100644 --- a/SignalRegistry/SignalRegistry.php +++ b/SignalRegistry/SignalRegistry.php @@ -105,4 +105,12 @@ public function popPreviousHandlers(): void } } } + + /** + * @internal + */ + public function scheduleAlarm(int $seconds): void + { + pcntl_alarm($seconds); + } } diff --git a/SingleCommandApplication.php b/SingleCommandApplication.php index ff1c17247..2b54fb870 100644 --- a/SingleCommandApplication.php +++ b/SingleCommandApplication.php @@ -67,6 +67,6 @@ public function run(?InputInterface $input = null, ?OutputInterface $output = nu $this->running = false; } - return $ret ?? 1; + return $ret; } } diff --git a/Style/OutputStyle.php b/Style/OutputStyle.php index ddfa8decc..89a3a4177 100644 --- a/Style/OutputStyle.php +++ b/Style/OutputStyle.php @@ -23,17 +23,12 @@ */ abstract class OutputStyle implements OutputInterface, StyleInterface { - private OutputInterface $output; - - public function __construct(OutputInterface $output) - { - $this->output = $output; + public function __construct( + private OutputInterface $output, + ) { } - /** - * @return void - */ - public function newLine(int $count = 1) + public function newLine(int $count = 1): void { $this->output->write(str_repeat(\PHP_EOL, $count)); } @@ -43,26 +38,17 @@ public function createProgressBar(int $max = 0): ProgressBar return new ProgressBar($this->output, $max); } - /** - * @return void - */ - public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) + public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL): void { $this->output->write($messages, $newline, $type); } - /** - * @return void - */ - public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL) + public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL): void { $this->output->writeln($messages, $type); } - /** - * @return void - */ - public function setVerbosity(int $level) + public function setVerbosity(int $level): void { $this->output->setVerbosity($level); } @@ -72,10 +58,7 @@ public function getVerbosity(): int return $this->output->getVerbosity(); } - /** - * @return void - */ - public function setDecorated(bool $decorated) + public function setDecorated(bool $decorated): void { $this->output->setDecorated($decorated); } @@ -85,10 +68,7 @@ public function isDecorated(): bool return $this->output->isDecorated(); } - /** - * @return void - */ - public function setFormatter(OutputFormatterInterface $formatter) + public function setFormatter(OutputFormatterInterface $formatter): void { $this->output->setFormatter($formatter); } @@ -98,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(); @@ -118,10 +104,7 @@ public function isDebug(): bool return $this->output->isDebug(); } - /** - * @return OutputInterface - */ - protected function getErrorOutput() + protected function getErrorOutput(): OutputInterface { if (!$this->output instanceof ConsoleOutputInterface) { return $this->output; diff --git a/Style/StyleInterface.php b/Style/StyleInterface.php index 6bced158a..fcc5bc775 100644 --- a/Style/StyleInterface.php +++ b/Style/StyleInterface.php @@ -20,73 +20,53 @@ interface StyleInterface { /** * Formats a command title. - * - * @return void */ - public function title(string $message); + public function title(string $message): void; /** * Formats a section title. - * - * @return void */ - public function section(string $message); + public function section(string $message): void; /** * Formats a list. - * - * @return void */ - public function listing(array $elements); + public function listing(array $elements): void; /** * Formats informational text. - * - * @return void */ - public function text(string|array $message); + public function text(string|array $message): void; /** * Formats a success result bar. - * - * @return void */ - public function success(string|array $message); + public function success(string|array $message): void; /** * Formats an error result bar. - * - * @return void */ - public function error(string|array $message); + public function error(string|array $message): void; /** * Formats an warning result bar. - * - * @return void */ - public function warning(string|array $message); + public function warning(string|array $message): void; /** * Formats a note admonition. - * - * @return void */ - public function note(string|array $message); + public function note(string|array $message): void; /** * Formats a caution admonition. - * - * @return void */ - public function caution(string|array $message); + public function caution(string|array $message): void; /** * Formats a table. - * - * @return void */ - public function table(array $headers, array $rows); + public function table(array $headers, array $rows): void; /** * Asks a question. @@ -110,29 +90,21 @@ public function choice(string $question, array $choices, mixed $default = null): /** * Add newline(s). - * - * @return void */ - public function newLine(int $count = 1); + public function newLine(int $count = 1): void; /** * Starts the progress output. - * - * @return void */ - public function progressStart(int $max = 0); + public function progressStart(int $max = 0): void; /** * Advances the progress output X steps. - * - * @return void */ - public function progressAdvance(int $step = 1); + public function progressAdvance(int $step = 1): void; /** * Finishes the progress output. - * - * @return void */ - public function progressFinish(); + public function progressFinish(): void; } diff --git a/Style/SymfonyStyle.php b/Style/SymfonyStyle.php index 135f4fd0e..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,30 +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. - * - * @return void */ - public function block(string|array $messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true) + public function block(string|array $messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true): void { $messages = \is_array($messages) ? array_values($messages) : [$messages]; @@ -72,10 +72,7 @@ public function block(string|array $messages, ?string $type = null, ?string $sty $this->newLine(); } - /** - * @return void - */ - public function title(string $message) + public function title(string $message): void { $this->autoPrependBlock(); $this->writeln([ @@ -85,10 +82,7 @@ public function title(string $message) $this->newLine(); } - /** - * @return void - */ - public function section(string $message) + public function section(string $message): void { $this->autoPrependBlock(); $this->writeln([ @@ -98,10 +92,7 @@ public function section(string $message) $this->newLine(); } - /** - * @return void - */ - public function listing(array $elements) + public function listing(array $elements): void { $this->autoPrependText(); $elements = array_map(fn ($element) => \sprintf(' * %s', $element), $elements); @@ -110,10 +101,7 @@ public function listing(array $elements) $this->newLine(); } - /** - * @return void - */ - public function text(string|array $message) + public function text(string|array $message): void { $this->autoPrependText(); @@ -125,68 +113,46 @@ public function text(string|array $message) /** * Formats a command comment. - * - * @return void */ - public function comment(string|array $message) + public function comment(string|array $message): void { $this->block($message, null, null, ' // ', false, false); } - /** - * @return void - */ - public function success(string|array $message) + public function success(string|array $message): void { $this->block($message, 'OK', 'fg=black;bg=green', ' ', true); } - /** - * @return void - */ - public function error(string|array $message) + public function error(string|array $message): void { $this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true); } - /** - * @return void - */ - public function warning(string|array $message) + public function warning(string|array $message): void { $this->block($message, 'WARNING', 'fg=black;bg=yellow', ' ', true); } - /** - * @return void - */ - public function note(string|array $message) + public function note(string|array $message): void { $this->block($message, 'NOTE', 'fg=yellow', ' ! '); } /** * Formats an info message. - * - * @return void */ - public function info(string|array $message) + public function info(string|array $message): void { $this->block($message, 'INFO', 'fg=green', ' ', true); } - /** - * @return void - */ - public function caution(string|array $message) + public function caution(string|array $message): void { $this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true); } - /** - * @return void - */ - public function table(array $headers, array $rows) + public function table(array $headers, array $rows): void { $this->createTable() ->setHeaders($headers) @@ -199,10 +165,8 @@ public function table(array $headers, array $rows) /** * Formats a horizontal table. - * - * @return void */ - public function horizontalTable(array $headers, array $rows) + public function horizontalTable(array $headers, array $rows): void { $this->createTable() ->setHorizontal(true) @@ -221,10 +185,8 @@ public function horizontalTable(array $headers, array $rows) * * 'A title' * * ['key' => 'value'] * * new TableSeparator() - * - * @return void */ - public function definitionList(string|array|TableSeparator ...$list) + public function definitionList(string|array|TableSeparator ...$list): void { $headers = []; $row = []; @@ -285,27 +247,18 @@ public function choice(string $question, array $choices, mixed $default = null, return $this->askQuestion($questionChoice); } - /** - * @return void - */ - public function progressStart(int $max = 0) + public function progressStart(int $max = 0): void { $this->progressBar = $this->createProgressBar($max); $this->progressBar->start(); } - /** - * @return void - */ - public function progressAdvance(int $step = 1) + public function progressAdvance(int $step = 1): void { $this->getProgressBar()->advance($step); } - /** - * @return void - */ - public function progressFinish() + public function progressFinish(): void { $this->getProgressBar()->finish(); $this->newLine(2); @@ -366,10 +319,7 @@ public function askQuestion(Question $question): mixed return $answer; } - /** - * @return void - */ - public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL) + public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL): void { if (!is_iterable($messages)) { $messages = [$messages]; @@ -381,10 +331,7 @@ public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORM } } - /** - * @return void - */ - public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) + public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL): void { if (!is_iterable($messages)) { $messages = [$messages]; @@ -396,10 +343,7 @@ public function write(string|iterable $messages, bool $newline = false, int $typ } } - /** - * @return void - */ - public function newLine(int $count = 1) + public function newLine(int $count = 1): void { parent::newLine($count); $this->bufferedOutput->write(str_repeat("\n", $count)); @@ -428,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); diff --git a/Terminal.php b/Terminal.php index f094adedc..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. */ 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/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index 5482fa238..7f8367b75 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; @@ -70,11 +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 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); } } @@ -191,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(); @@ -286,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() @@ -851,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'); @@ -1267,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']); @@ -1290,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']); @@ -1427,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); @@ -1483,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); @@ -1551,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); @@ -1663,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); @@ -1690,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); @@ -1720,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); @@ -1850,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; }); }, ])); @@ -1926,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); @@ -2281,6 +2310,24 @@ public function testSignalHandlersAreCleanedUpAfterCommandRuns() $this->assertCount(0, $this->getHandlersForSignal($signalRegistry, \SIGUSR1), 'Registry should still be empty after second run.'); } + /** + * @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 */ @@ -2317,6 +2364,117 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->assertCount(0, $this->getHandlersForSignal($signalRegistry, \SIGUSR1), 'Signal handlers must be cleaned up even on exception.'); } + /** + * @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 */ @@ -2403,6 +2561,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->assertCount(0, $this->getHandlersForSignal($signalRegistry, \SIGUSR1), 'Registry must be empty after all commands are finished.'); } + /** + * @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 */ @@ -2471,6 +2648,151 @@ public function testsPreservedHelpOptionWhenItsAnAlternative() $this->assertSame(Command::SUCCESS, $tester->getStatusCode()); } + /** + * @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); + } + + 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); + } + /** * Reads the private "signalHandlers" property of the SignalRegistry for assertions. */ @@ -2488,7 +2810,7 @@ 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; } @@ -2578,7 +2900,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 { @@ -2595,7 +2917,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 { @@ -2612,7 +2934,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; @@ -2678,3 +3000,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 58fbfcf79..0db3572fc 100644 --- a/Tests/Command/CommandTest.php +++ b/Tests/Command/CommandTest.php @@ -12,12 +12,13 @@ namespace Symfony\Component\Console\Tests\Command; use PHPUnit\Framework\TestCase; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +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; @@ -29,7 +30,7 @@ class CommandTest extends TestCase { - use ExpectDeprecationTrait; + use ExpectUserDeprecationMessageTrait; protected static string $fixturesPath; @@ -350,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); @@ -396,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; }; } @@ -411,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([]); @@ -430,13 +439,11 @@ 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()); } @@ -444,70 +451,85 @@ public function testCommandAttribute() /** * @group legacy */ - public function testDefaultNameProperty() + public function testCommandAttributeWithDeprecatedMethods() { - $this->expectDeprecation('Since symfony/console 6.1: Relying on the static property "$defaultName" for setting a command name is deprecated. Add the "Symfony\Component\Console\Attribute\AsCommand" attribute to the "Symfony\Component\Console\Tests\Command\MyCommand" class instead.'); + $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', MyCommand::getDefaultName()); + $this->assertSame('|foo|f', Php8Command::getDefaultName()); + $this->assertSame('desc', Php8Command::getDefaultDescription()); } - /** - * @group legacy - */ - public function testDefaultDescriptionProperty() + public function testAttributeOverridesProperty() { - $this->expectDeprecation('Since symfony/console 6.1: Relying on the static property "$defaultDescription" for setting a command description is deprecated. Add the "Symfony\Component\Console\Attribute\AsCommand" attribute to the "Symfony\Component\Console\Tests\Command\MyCommand" class instead.'); + $command = new MyAnnotatedCommand(); - $this->assertSame('This is a command I wrote all by myself', MyCommand::getDefaultDescription()); + $this->assertSame('my:command', $command->getName()); + $this->assertSame('This is a command I wrote all by myself', $command->getDescription()); } /** * @group legacy */ - public function testStaticDefaultProperties() + public function testAttributeOverridesPropertyWithDeprecatedMethods() { - $command = new MyCommand(); + $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', $command->getName()); - $this->assertSame('This is a command I wrote all by myself', $command->getDescription()); - } - - public function testAttributeOverridesProperty() - { $this->assertSame('my:command', MyAnnotatedCommand::getDefaultName()); $this->assertSame('This is a command I wrote all by myself', MyAnnotatedCommand::getDefaultDescription()); - - $command = new MyAnnotatedCommand(); - - $this->assertSame('my:command', $command->getName()); - $this->assertSame('This is a command I wrote all by myself', $command->getDescription()); } public function testDefaultCommand() { $apl = new Application(); - $apl->setDefaultCommand(Php8Command::getDefaultName()); + $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 { } @@ -517,29 +539,23 @@ class Php8Command2 extends Command { } -class MyCommand extends Command -{ - /** - * @deprecated since Symfony 6.1 - */ - protected static $defaultName = 'my:command'; - - /** - * @deprecated since Symfony 6.1 - */ - protected static $defaultDescription = 'This is a command I wrote all by myself'; -} - #[AsCommand(name: 'my:command', description: 'This is a command I wrote all by myself')] class MyAnnotatedCommand extends Command { - /** - * @deprecated since Symfony 6.1 - */ protected static $defaultName = 'i-shall-be-ignored'; - /** - * @deprecated since Symfony 6.1 - */ 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 7d18d01c4..72db26697 100644 --- a/Tests/Command/CompleteCommandTest.php +++ b/Tests/Command/CompleteCommandTest.php @@ -56,6 +56,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); @@ -64,8 +66,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); } /** @@ -122,9 +122,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', '--global-option']]; + yield 'definition' => [['bin/console', 'hello', '-'], ['--help', '--silent', '--quiet', '--verbose', '--version', '--ansi', '--no-ansi', '--no-interaction', '--global-option']]; yield 'custom' => [['bin/console', 'hello'], ['Fabien', 'Robin', 'Wouter']]; - yield 'definition-aliased' => [['bin/console', 'ahoy', '-'], ['--help', '--quiet', '--verbose', '--version', '--ansi', '--no-ansi', '--no-interaction', '--global-option']]; + yield 'definition-aliased' => [['bin/console', 'ahoy', '-'], ['--help', '--silent', '--quiet', '--verbose', '--version', '--ansi', '--no-ansi', '--no-interaction', '--global-option']]; yield 'custom-aliased' => [['bin/console', 'ahoy'], ['Fabien', 'Robin', 'Wouter']]; yield 'global-option-values' => [['bin/console', '--global-option'], ['foo', 'bar', 'baz']]; yield 'global-option-with-command-values' => [['bin/console', 'ahoy', '--global-option'], ['foo', 'bar', 'baz']]; 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/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 9c04d2706..408f8c0d3 100644 --- a/Tests/ConsoleEventsTest.php +++ b/Tests/ConsoleEventsTest.php @@ -39,6 +39,7 @@ protected function tearDown(): void 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..393dfacf2 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,67 @@ 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 testProcessCommandWithDescriptionWithpercentageSigns() + { + $container = new ContainerBuilder(); + $container + ->register( + 'description_with_percentage_signs_command', + DescriptionWithPercentageSignsCommand::class, + ) + ->addTag('console.command') + ; + $pass = new AddConsoleCommandPass(); + $pass->process($container); + + $command = $container->get('console.command_loader')->get('description-percentage-signs'); + + self::assertTrue($container->has('description_with_percentage_signs_command.command')); + self::assertSame('Just testing %percentage-signs%', $command->getDescription()); + } + + 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 +377,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 +394,37 @@ 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: 'description-percentage-signs', description: 'Just testing %percentage-signs%')] +class DescriptionWithPercentageSignsCommand +{ + 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/EventListener/ErrorListenerTest.php b/Tests/EventListener/ErrorListenerTest.php index 40020baee..e26109851 100644 --- a/Tests/EventListener/ErrorListenerTest.php +++ b/Tests/EventListener/ErrorListenerTest.php @@ -107,19 +107,6 @@ public function testAllKindsOfInputCanBeLogged() $listener->onConsoleTerminate($this->getConsoleTerminateEvent(new StringInput('test:run --foo=bar'), 255)); } - public function testCommandNameIsDisplayedForNonStringableInput() - { - $logger = $this->createMock(LoggerInterface::class); - $logger - ->expects($this->once()) - ->method('debug') - ->with('Command "{command}" exited with code "{code}"', ['command' => 'test:run', 'code' => 255]) - ; - - $listener = new ErrorListener($logger); - $listener->onConsoleTerminate($this->getConsoleTerminateEvent($this->createMock(InputInterface::class), 255)); - } - private function getConsoleTerminateEvent(InputInterface $input, $exitCode) { return new ConsoleTerminateEvent(new Command('test:run'), $input, $this->createMock(OutputInterface::class), $exitCode); 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/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 33a90e356..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": { @@ -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": { @@ -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": { @@ -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": { @@ -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": { @@ -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": { @@ -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": { @@ -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": { 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.0 Options: -h, --help Display help for the given command. When no command is given display help for the list command - -q, --quiet Do not output any message + --silent Do not output any message + -q, --quiet Only errors are displayed. All other output is suppressed -V, --version Display this application version --ansi|--no-ansi Force (or disable --no-ansi) ANSI output -n, --no-interaction Do not ask any interactive question diff --git a/Tests/Fixtures/application_2.xml b/Tests/Fixtures/application_2.xml index 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.0 Options: -h, --help Display help for the given command. When no command is given display help for the list command - -q, --quiet Do not output any message + --silent Do not output any message + -q, --quiet Only errors are displayed. All other output is suppressed -V, --version Display this application version --ansi|--no-ansi Force (or disable --no-ansi) ANSI output -n, --no-interaction Do not ask any interactive question diff --git a/Tests/Fixtures/application_mbstring.md b/Tests/Fixtures/application_mbstring.md index 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 9cbdd8e78..6916694eb 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/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 af7cf2ef5..c0278cc33 100644 --- a/Tests/Helper/ProgressBarTest.php +++ b/Tests/Helper/ProgressBarTest.php @@ -1033,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); @@ -1047,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()) ); @@ -1062,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()) ); @@ -1097,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()) ); } @@ -1177,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); @@ -1348,7 +1362,7 @@ public function testMultiLineFormatIsFullyCorrectlyWithManuallyCleanup() 'Foo!'.\PHP_EOL. $this->generateOutput('[--->------------------------]'). "\nProcessing \"foobar\"...". - $this->generateOutput("[----->----------------------]\nProcessing \"foobar\"..."), + $this->generateOutput("[============================]\nProcessing \"foobar\"..."), stream_get_contents($output->getStream()) ); } diff --git a/Tests/Helper/ProgressIndicatorTest.php b/Tests/Helper/ProgressIndicatorTest.php index 8e2e10e37..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); diff --git a/Tests/Helper/QuestionHelperTest.php b/Tests/Helper/QuestionHelperTest.php index 4a89829d7..52d13c1b1 100644 --- a/Tests/Helper/QuestionHelperTest.php +++ b/Tests/Helper/QuestionHelperTest.php @@ -779,7 +779,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; 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 ebdfa5a5d..eb85364da 100644 --- a/Tests/Helper/TableTest.php +++ b/Tests/Helper/TableTest.php @@ -112,6 +112,20 @@ public static function renderProvider() | 80-902734-1-6 | And Then There Were None | Agatha Christie | +---------------+--------------------------+------------------+ +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, ], [ 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 137834655..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() @@ -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 7e3fb16da..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,11 +52,11 @@ public function testBooleanWithOptional() public function testShortcut() { $option = new InputOption('foo', 'f'); - $this->assertEquals('f', $option->getShortcut(), '__construct() can take a shortcut as its second argument'); + $this->assertSame('f', $option->getShortcut(), '__construct() can take a shortcut as its second argument'); $option = new InputOption('foo', '-f|-ff|fff'); - $this->assertEquals('f|ff|fff', $option->getShortcut(), '__construct() removes the leading - of the shortcuts'); + $this->assertSame('f|ff|fff', $option->getShortcut(), '__construct() removes the leading - of the shortcuts'); $option = new InputOption('foo', ['f', 'ff', '-fff']); - $this->assertEquals('f|ff|fff', $option->getShortcut(), '__construct() removes the leading - of the shortcuts'); + $this->assertSame('f|ff|fff', $option->getShortcut(), '__construct() removes the leading - of the shortcuts'); $option = new InputOption('foo'); $this->assertNull($option->getShortcut(), '__construct() makes the shortcut null by default'); $option = new InputOption('foo', ''); @@ -64,15 +64,15 @@ public function testShortcut() $option = new InputOption('foo', []); $this->assertNull($option->getShortcut(), '__construct() makes the shortcut null when given an empty array'); $option = new InputOption('foo', ['f', '', 'fff']); - $this->assertEquals('f|fff', $option->getShortcut(), '__construct() removes empty shortcuts'); + $this->assertSame('f|fff', $option->getShortcut(), '__construct() removes empty shortcuts'); $option = new InputOption('foo', 'f||fff'); - $this->assertEquals('f|fff', $option->getShortcut(), '__construct() removes empty shortcuts'); + $this->assertSame('f|fff', $option->getShortcut(), '__construct() removes empty shortcuts'); $option = new InputOption('foo', '0'); - $this->assertEquals('0', $option->getShortcut(), '-0 is an acceptable shortcut value'); + $this->assertSame('0', $option->getShortcut(), '-0 is an acceptable shortcut value'); $option = new InputOption('foo', ['0', 'z']); - $this->assertEquals('0|z', $option->getShortcut(), '-0 is an acceptable shortcut value when embedded in an array'); + $this->assertSame('0|z', $option->getShortcut(), '-0 is an acceptable shortcut value when embedded in an array'); $option = new InputOption('foo', '0|z'); - $this->assertEquals('0|z', $option->getShortcut(), '-0 is an acceptable shortcut value when embedded in a string-list'); + $this->assertSame('0|z', $option->getShortcut(), '-0 is an acceptable shortcut value when embedded in a string-list'); $option = new InputOption('foo', false); $this->assertNull($option->getShortcut(), '__construct() makes the shortcut null when given a false as value'); } @@ -142,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'); @@ -169,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/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/QuestionTest.php b/Tests/Question/QuestionTest.php index 0bc6f75db..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'); } diff --git a/Tests/SignalRegistry/SignalRegistryTest.php b/Tests/SignalRegistry/SignalRegistryTest.php index 77b7e28e7..8a48bc287 100644 --- a/Tests/SignalRegistry/SignalRegistryTest.php +++ b/Tests/SignalRegistry/SignalRegistryTest.php @@ -27,6 +27,7 @@ protected function tearDown(): void 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 799618a1e..d36042038 100644 --- a/Tests/Tester/CommandTesterTest.php +++ b/Tests/Tester/CommandTesterTest.php @@ -34,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]); @@ -94,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); @@ -114,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); @@ -165,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); @@ -190,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); @@ -217,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); @@ -242,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); @@ -261,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 1610f7341..65d69913a 100644 --- a/composer.json +++ b/composer.json @@ -16,34 +16,34 @@ } ], "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" + "symfony/string": "^7.2" }, "require-dev": { - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", "symfony/http-foundation": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", "psr/log": "^1|^2|^3" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "autoload": { "psr-4": { "Symfony\\Component\\Console\\": "" },