Upgrade framework

This commit is contained in:
2023-11-14 16:54:35 +01:00
parent 1648a5cd42
commit 4fcf6fffcc
10548 changed files with 693138 additions and 466698 deletions

View File

@@ -11,16 +11,19 @@
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Base class for all commands.
@@ -29,42 +32,91 @@ use Symfony\Component\Console\Exception\LogicException;
*/
class Command
{
private $application;
private $name;
private $processTitle;
private $aliases = array();
private $definition;
private $hidden = false;
private $help;
private $description;
private $ignoreValidationErrors = false;
private $applicationDefinitionMerged = false;
private $applicationDefinitionMergedWithArgs = false;
private $code;
private $synopsis = array();
private $usages = array();
private $helperSet;
// 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
*/
protected static $defaultName;
/**
* @var string|null The default command description
*/
protected static $defaultDescription;
private $application = null;
private ?string $name = null;
private ?string $processTitle = null;
private array $aliases = [];
private $definition;
private bool $hidden = false;
private string $help = '';
private string $description = '';
private $fullDefinition = null;
private bool $ignoreValidationErrors = false;
private ?\Closure $code = null;
private array $synopsis = [];
private array $usages = [];
private $helperSet = null;
public static function getDefaultName(): ?string
{
$class = static::class;
if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
return $attribute[0]->newInstance()->name;
}
$r = new \ReflectionProperty($class, 'defaultName');
return $class === $r->class ? static::$defaultName : null;
}
public static function getDefaultDescription(): ?string
{
$class = static::class;
if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
return $attribute[0]->newInstance()->description;
}
$r = new \ReflectionProperty($class, 'defaultDescription');
return $class === $r->class ? static::$defaultDescription : null;
}
/**
* Constructor.
*
* @param string|null $name The name of the command; passing null means it must be set in configure()
*
* @throws LogicException When the command name is empty
*/
public function __construct($name = null)
public function __construct(string $name = null)
{
$this->definition = new InputDefinition();
if (null === $name && null !== $name = static::getDefaultName()) {
$aliases = explode('|', $name);
if ('' === $name = array_shift($aliases)) {
$this->setHidden(true);
$name = array_shift($aliases);
}
$this->setAliases($aliases);
}
if (null !== $name) {
$this->setName($name);
}
$this->configure();
if (!$this->name) {
throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this)));
if ('' === $this->description) {
$this->setDescription(static::getDefaultDescription() ?? '');
}
$this->configure();
}
/**
@@ -77,11 +129,6 @@ class Command
$this->ignoreValidationErrors = true;
}
/**
* Sets the application instance for this command.
*
* @param Application $application An Application instance
*/
public function setApplication(Application $application = null)
{
$this->application = $application;
@@ -90,13 +137,10 @@ class Command
} else {
$this->helperSet = null;
}
$this->fullDefinition = null;
}
/**
* Sets the helper set.
*
* @param HelperSet $helperSet A HelperSet instance
*/
public function setHelperSet(HelperSet $helperSet)
{
$this->helperSet = $helperSet;
@@ -104,20 +148,16 @@ class Command
/**
* Gets the helper set.
*
* @return HelperSet A HelperSet instance
*/
public function getHelperSet()
public function getHelperSet(): ?HelperSet
{
return $this->helperSet;
}
/**
* Gets the application instance for this command.
*
* @return Application An Application instance
*/
public function getApplication()
public function getApplication(): ?Application
{
return $this->application;
}
@@ -125,7 +165,7 @@ class Command
/**
* Checks whether the command is enabled or not in the current environment.
*
* Override this to check for x or y and return false if the command can not
* Override this to check for x or y and return false if the command cannot
* run properly under the current conditions.
*
* @return bool
@@ -150,10 +190,7 @@ class Command
* execute() method, you set the code to execute by passing
* a Closure to the setCode() method.
*
* @param InputInterface $input An InputInterface instance
* @param OutputInterface $output An OutputInterface instance
*
* @return null|int null or 0 if everything went fine, or an error code
* @return int 0 if everything went fine, or an exit code
*
* @throws LogicException When this abstract method is not implemented
*
@@ -170,22 +207,20 @@ class Command
* This method is executed before the InputDefinition is validated.
* This means that this is the only place where the command can
* interactively ask for values of missing required arguments.
*
* @param InputInterface $input An InputInterface instance
* @param OutputInterface $output An OutputInterface instance
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
}
/**
* Initializes the command just after the input has been validated.
* Initializes the command after the input has been bound and before the input
* is validated.
*
* This is mainly useful when a lot of commands extends one main command
* where some things need to be initialized based on the input arguments and options.
*
* @param InputInterface $input An InputInterface instance
* @param OutputInterface $output An OutputInterface instance
* @see InputInterface::bind()
* @see InputInterface::validate()
*/
protected function initialize(InputInterface $input, OutputInterface $output)
{
@@ -198,28 +233,21 @@ class Command
* setCode() method or by overriding the execute() method
* in a sub-class.
*
* @param InputInterface $input An InputInterface instance
* @param OutputInterface $output An OutputInterface instance
*
* @return int The command exit code
*
* @throws \Exception When binding input fails. Bypass this by calling {@link ignoreValidationErrors()}.
* @throws ExceptionInterface When input binding fails. Bypass this by calling {@link ignoreValidationErrors()}.
*
* @see setCode()
* @see execute()
*/
public function run(InputInterface $input, OutputInterface $output)
public function run(InputInterface $input, OutputInterface $output): int
{
// force the creation of the synopsis before the merge with the app definition
$this->getSynopsis(true);
$this->getSynopsis(false);
// add the application arguments and options
$this->mergeApplicationDefinition();
// bind the input against the command specific arguments/options
try {
$input->bind($this->definition);
$input->bind($this->getDefinition());
} catch (ExceptionInterface $e) {
if (!$this->ignoreValidationErrors) {
throw $e;
@@ -229,16 +257,15 @@ class Command
$this->initialize($input, $output);
if (null !== $this->processTitle) {
if (function_exists('cli_set_process_title')) {
if (false === @cli_set_process_title($this->processTitle)) {
if ('Darwin' === PHP_OS) {
$output->writeln('<comment>Running "cli_get_process_title" as an unprivileged user is not supported on MacOS.</comment>');
if (\function_exists('cli_set_process_title')) {
if (!@cli_set_process_title($this->processTitle)) {
if ('Darwin' === \PHP_OS) {
$output->writeln('<comment>Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.</comment>', OutputInterface::VERBOSITY_VERY_VERBOSE);
} else {
$error = error_get_last();
trigger_error($error['message'], E_USER_WARNING);
cli_set_process_title($this->processTitle);
}
}
} elseif (function_exists('setproctitle')) {
} elseif (\function_exists('setproctitle')) {
setproctitle($this->processTitle);
} elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) {
$output->writeln('<comment>Install the proctitle PECL to be able to change the process title.</comment>');
@@ -259,14 +286,25 @@ class Command
$input->validate();
if ($this->code) {
$statusCode = call_user_func($this->code, $input, $output);
$statusCode = ($this->code)($input, $output);
} else {
$statusCode = $this->execute($input, $output);
if (!\is_int($statusCode)) {
throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode)));
}
}
return is_numeric($statusCode) ? (int) $statusCode : 0;
}
/**
* Adds suggestions to $suggestions for the current completion input (e.g. option or argument).
*/
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
}
/**
* Sets the code to execute when running this command.
*
@@ -281,21 +319,22 @@ class Command
*
* @see execute()
*/
public function setCode(callable $code)
public function setCode(callable $code): static
{
if ($code instanceof \Closure) {
$r = new \ReflectionFunction($code);
if (null === $r->getClosureThis()) {
if (\PHP_VERSION_ID < 70000) {
// Bug in PHP5: https://bugs.php.net/bug.php?id=64761
// This means that we cannot bind static closures and therefore we must
// ignore any errors here. There is no way to test if the closure is
// bindable.
$code = @\Closure::bind($code, $this);
} else {
$code = \Closure::bind($code, $this);
set_error_handler(static function () {});
try {
if ($c = \Closure::bind($code, $this)) {
$code = $c;
}
} finally {
restore_error_handler();
}
}
} else {
$code = \Closure::fromCallable($code);
}
$this->code = $code;
@@ -309,35 +348,33 @@ class Command
* This method is not part of public API and should not be used directly.
*
* @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments
*
* @internal
*/
public function mergeApplicationDefinition($mergeArgs = true)
public function mergeApplicationDefinition(bool $mergeArgs = true)
{
if (null === $this->application || (true === $this->applicationDefinitionMerged && ($this->applicationDefinitionMergedWithArgs || !$mergeArgs))) {
if (null === $this->application) {
return;
}
$this->definition->addOptions($this->application->getDefinition()->getOptions());
$this->fullDefinition = new InputDefinition();
$this->fullDefinition->setOptions($this->definition->getOptions());
$this->fullDefinition->addOptions($this->application->getDefinition()->getOptions());
if ($mergeArgs) {
$currentArguments = $this->definition->getArguments();
$this->definition->setArguments($this->application->getDefinition()->getArguments());
$this->definition->addArguments($currentArguments);
}
$this->applicationDefinitionMerged = true;
if ($mergeArgs) {
$this->applicationDefinitionMergedWithArgs = true;
$this->fullDefinition->setArguments($this->application->getDefinition()->getArguments());
$this->fullDefinition->addArguments($this->definition->getArguments());
} else {
$this->fullDefinition->setArguments($this->definition->getArguments());
}
}
/**
* Sets an array of argument and option instances.
*
* @param array|InputDefinition $definition An array of argument and option instances or a definition instance
*
* @return $this
*/
public function setDefinition($definition)
public function setDefinition(array|InputDefinition $definition): static
{
if ($definition instanceof InputDefinition) {
$this->definition = $definition;
@@ -345,19 +382,17 @@ class Command
$this->definition->setDefinition($definition);
}
$this->applicationDefinitionMerged = false;
$this->fullDefinition = null;
return $this;
}
/**
* Gets the InputDefinition attached to this Command.
*
* @return InputDefinition An InputDefinition instance
*/
public function getDefinition()
public function getDefinition(): InputDefinition
{
return $this->definition;
return $this->fullDefinition ?? $this->getNativeDefinition();
}
/**
@@ -367,27 +402,28 @@ class Command
* be changed by merging with the application InputDefinition.
*
* This method is not part of public API and should not be used directly.
*
* @return InputDefinition An InputDefinition instance
*/
public function getNativeDefinition()
public function getNativeDefinition(): InputDefinition
{
return $this->getDefinition();
return $this->definition ?? throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
}
/**
* Adds an argument.
*
* @param string $name The argument name
* @param int $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
* @param string $description A description text
* @param mixed $default The default value (for InputArgument::OPTIONAL mode only)
* @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
* @param mixed $default The default value (for InputArgument::OPTIONAL mode only)
*
* @throws InvalidArgumentException When argument mode is not valid
*
* @return $this
*/
public function addArgument($name, $mode = null, $description = '', $default = null)
public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null): static
{
$this->definition->addArgument(new InputArgument($name, $mode, $description, $default));
if (null !== $this->fullDefinition) {
$this->fullDefinition->addArgument(new InputArgument($name, $mode, $description, $default));
}
return $this;
}
@@ -395,17 +431,20 @@ class Command
/**
* Adds an option.
*
* @param string $name The option name
* @param string $shortcut The shortcut (can be null)
* @param int $mode The option mode: One of the InputOption::VALUE_* constants
* @param string $description A description text
* @param mixed $default The default value (must be null for InputOption::VALUE_NONE)
* @param $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
* @param $mode The option mode: One of the InputOption::VALUE_* constants
* @param $default The default value (must be null for InputOption::VALUE_NONE)
*
* @throws InvalidArgumentException If option mode is invalid or incompatible
*
* @return $this
*/
public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null)
public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null): static
{
$this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
if (null !== $this->fullDefinition) {
$this->fullDefinition->addOption(new InputOption($name, $shortcut, $mode, $description, $default));
}
return $this;
}
@@ -418,13 +457,11 @@ class Command
*
* $command->setName('foo:bar');
*
* @param string $name The command name
*
* @return $this
*
* @throws InvalidArgumentException When the name is invalid
*/
public function setName($name)
public function setName(string $name): static
{
$this->validateName($name);
@@ -439,13 +476,9 @@ class Command
* This feature should be used only when creating a long process command,
* like a daemon.
*
* PHP 5.5+ or the proctitle PECL library is required
*
* @param string $title The process title
*
* @return $this
*/
public function setProcessTitle($title)
public function setProcessTitle(string $title): static
{
$this->processTitle = $title;
@@ -454,10 +487,8 @@ class Command
/**
* Returns the command name.
*
* @return string The command name
*/
public function getName()
public function getName(): ?string
{
return $this->name;
}
@@ -465,19 +496,19 @@ class Command
/**
* @param bool $hidden Whether or not the command should be hidden from the list of commands
*
* @return Command The current instance
* @return $this
*/
public function setHidden($hidden)
public function setHidden(bool $hidden = true): static
{
$this->hidden = (bool) $hidden;
$this->hidden = $hidden;
return $this;
}
/**
* @return bool Whether the command should be publicly shown or not.
* @return bool whether the command should be publicly shown or not
*/
public function isHidden()
public function isHidden(): bool
{
return $this->hidden;
}
@@ -485,11 +516,9 @@ class Command
/**
* Sets the description for the command.
*
* @param string $description The description for the command
*
* @return $this
*/
public function setDescription($description)
public function setDescription(string $description): static
{
$this->description = $description;
@@ -498,10 +527,8 @@ class Command
/**
* Returns the description for the command.
*
* @return string The description for the command
*/
public function getDescription()
public function getDescription(): string
{
return $this->description;
}
@@ -509,11 +536,9 @@ class Command
/**
* Sets the help for the command.
*
* @param string $help The help for the command
*
* @return $this
*/
public function setHelp($help)
public function setHelp(string $help): static
{
$this->help = $help;
@@ -522,10 +547,8 @@ class Command
/**
* Returns the help for the command.
*
* @return string The help for the command
*/
public function getHelp()
public function getHelp(): string
{
return $this->help;
}
@@ -533,21 +556,20 @@ class Command
/**
* Returns the processed help for the command replacing the %command.name% and
* %command.full_name% patterns with the real values dynamically.
*
* @return string The processed help for the command
*/
public function getProcessedHelp()
public function getProcessedHelp(): string
{
$name = $this->name;
$isSingleCommand = $this->application && $this->application->isSingleCommand();
$placeholders = array(
$placeholders = [
'%command.name%',
'%command.full_name%',
);
$replacements = array(
];
$replacements = [
$name,
$_SERVER['PHP_SELF'].' '.$name,
);
$isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name,
];
return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription());
}
@@ -561,27 +583,24 @@ class Command
*
* @throws InvalidArgumentException When an alias is invalid
*/
public function setAliases($aliases)
public function setAliases(iterable $aliases): static
{
if (!is_array($aliases) && !$aliases instanceof \Traversable) {
throw new InvalidArgumentException('$aliases must be an array or an instance of \Traversable');
}
$list = [];
foreach ($aliases as $alias) {
$this->validateName($alias);
$list[] = $alias;
}
$this->aliases = $aliases;
$this->aliases = \is_array($aliases) ? $aliases : $list;
return $this;
}
/**
* Returns the aliases for the command.
*
* @return array An array of aliases for the command
*/
public function getAliases()
public function getAliases(): array
{
return $this->aliases;
}
@@ -590,10 +609,8 @@ class Command
* Returns the synopsis for the command.
*
* @param bool $short Whether to show the short version of the synopsis (with options folded) or not
*
* @return string The synopsis
*/
public function getSynopsis($short = false)
public function getSynopsis(bool $short = false): string
{
$key = $short ? 'short' : 'long';
@@ -605,15 +622,13 @@ class Command
}
/**
* Add a command usage example.
*
* @param string $usage The usage, it'll be prefixed with the command name
* Add a command usage example, it'll be prefixed with the command name.
*
* @return $this
*/
public function addUsage($usage)
public function addUsage(string $usage): static
{
if (0 !== strpos($usage, $this->name)) {
if (!str_starts_with($usage, $this->name)) {
$usage = sprintf('%s %s', $this->name, $usage);
}
@@ -624,10 +639,8 @@ class Command
/**
* Returns alternative usages of the command.
*
* @return array
*/
public function getUsages()
public function getUsages(): array
{
return $this->usages;
}
@@ -635,14 +648,10 @@ class Command
/**
* Gets a helper instance by name.
*
* @param string $name The helper name
*
* @return mixed The helper value
*
* @throws LogicException if no HelperSet is defined
* @throws InvalidArgumentException if the helper is not defined
*/
public function getHelper($name)
public function getHelper(string $name): mixed
{
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));
@@ -656,11 +665,9 @@ class Command
*
* It must be non-empty and parts can optionally be separated by ":".
*
* @param string $name
*
* @throws InvalidArgumentException When the name is invalid
*/
private function validateName($name)
private function validateName(string $name)
{
if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) {
throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name));

View File

@@ -0,0 +1,205 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Output\BashCompletionOutput;
use Symfony\Component\Console\Completion\Output\CompletionOutputInterface;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Responsible for providing the values to the shell completion.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class CompleteCommand extends Command
{
protected static $defaultName = '|_complete';
protected static $defaultDescription = 'Internal command to provide shell completion suggestions';
private $completionOutputs;
private $isDebug = false;
/**
* @param array<string, class-string<CompletionOutputInterface>> $completionOutputs A list of additional completion outputs, with shell name as key and FQCN as value
*/
public function __construct(array $completionOutputs = [])
{
// must be set before the parent constructor, as the property value is used in configure()
$this->completionOutputs = $completionOutputs + ['bash' => BashCompletionOutput::class];
parent::__construct();
}
protected function configure(): void
{
$this
->addOption('shell', 's', InputOption::VALUE_REQUIRED, 'The shell type ("'.implode('", "', array_keys($this->completionOutputs)).'")')
->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'An array of input tokens (e.g. COMP_WORDS or argv)')
->addOption('current', 'c', InputOption::VALUE_REQUIRED, 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)')
->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'The version of the completion script')
;
}
protected function initialize(InputInterface $input, OutputInterface $output)
{
$this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOLEAN);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
try {
// uncomment when a bugfix or BC break has been introduced in the shell completion scripts
// $version = $input->getOption('symfony');
// if ($version && version_compare($version, 'x.y', '>=')) {
// $message = sprintf('Completion script version is not supported ("%s" given, ">=x.y" required).', $version);
// $this->log($message);
// $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.');
// return 126;
// }
$shell = $input->getOption('shell');
if (!$shell) {
throw new \RuntimeException('The "--shell" option must be set.');
}
if (!$completionOutput = $this->completionOutputs[$shell] ?? false) {
throw new \RuntimeException(sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode('", "', array_keys($this->completionOutputs))));
}
$completionInput = $this->createCompletionInput($input);
$suggestions = new CompletionSuggestions();
$this->log([
'',
'<comment>'.date('Y-m-d H:i:s').'</>',
'<info>Input:</> <comment>("|" indicates the cursor position)</>',
' '.(string) $completionInput,
'<info>Command:</>',
' '.(string) implode(' ', $_SERVER['argv']),
'<info>Messages:</>',
]);
$command = $this->findCommand($completionInput, $output);
if (null === $command) {
$this->log(' No command found, completing using the Application class.');
$this->getApplication()->complete($completionInput, $suggestions);
} elseif (
$completionInput->mustSuggestArgumentValuesFor('command')
&& $command->getName() !== $completionInput->getCompletionValue()
&& !\in_array($completionInput->getCompletionValue(), $command->getAliases(), true)
) {
$this->log(' No command found, completing using the Application class.');
// expand shortcut names ("cache:cl<TAB>") into their full name ("cache:clear")
$suggestions->suggestValues(array_filter(array_merge([$command->getName()], $command->getAliases())));
} else {
$command->mergeApplicationDefinition();
$completionInput->bind($command->getDefinition());
if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) {
$this->log(' Completing option names for the <comment>'.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).'</> command.');
$suggestions->suggestOptions($command->getDefinition()->getOptions());
} else {
$this->log([
' Completing using the <comment>'.\get_class($command instanceof LazyCommand ? $command->getCommand() : $command).'</> class.',
' Completing <comment>'.$completionInput->getCompletionType().'</> for <comment>'.$completionInput->getCompletionName().'</>',
]);
if (null !== $compval = $completionInput->getCompletionValue()) {
$this->log(' Current value: <comment>'.$compval.'</>');
}
$command->complete($completionInput, $suggestions);
}
}
/** @var CompletionOutputInterface $completionOutput */
$completionOutput = new $completionOutput();
$this->log('<info>Suggestions:</>');
if ($options = $suggestions->getOptionSuggestions()) {
$this->log(' --'.implode(' --', array_map(function ($o) { return $o->getName(); }, $options)));
} elseif ($values = $suggestions->getValueSuggestions()) {
$this->log(' '.implode(' ', $values));
} else {
$this->log(' <comment>No suggestions were provided</>');
}
$completionOutput->write($suggestions, $output);
} catch (\Throwable $e) {
$this->log([
'<error>Error!</error>',
(string) $e,
]);
if ($output->isDebug()) {
throw $e;
}
return self::FAILURE;
}
return self::SUCCESS;
}
private function createCompletionInput(InputInterface $input): CompletionInput
{
$currentIndex = $input->getOption('current');
if (!$currentIndex || !ctype_digit($currentIndex)) {
throw new \RuntimeException('The "--current" option must be set and it must be an integer.');
}
$completionInput = CompletionInput::fromTokens($input->getOption('input'), (int) $currentIndex);
try {
$completionInput->bind($this->getApplication()->getDefinition());
} catch (ExceptionInterface $e) {
}
return $completionInput;
}
private function findCommand(CompletionInput $completionInput, OutputInterface $output): ?Command
{
try {
$inputName = $completionInput->getFirstArgument();
if (null === $inputName) {
return null;
}
return $this->getApplication()->find($inputName);
} catch (CommandNotFoundException $e) {
}
return null;
}
private function log($messages): void
{
if (!$this->isDebug) {
return;
}
$commandName = basename($_SERVER['argv'][0]);
file_put_contents(sys_get_temp_dir().'/sf_'.$commandName.'.log', implode(\PHP_EOL, (array) $messages).\PHP_EOL, \FILE_APPEND);
}
}

View File

@@ -0,0 +1,139 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Process;
/**
* Dumps the completion script for the current shell.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class DumpCompletionCommand extends Command
{
protected static $defaultName = 'completion';
protected static $defaultDescription = 'Dump the shell completion script';
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('shell')) {
$suggestions->suggestValues($this->getSupportedShells());
}
}
protected function configure()
{
$fullCommand = $_SERVER['PHP_SELF'];
$commandName = basename($fullCommand);
$fullCommand = @realpath($fullCommand) ?: $fullCommand;
$this
->setHelp(<<<EOH
The <info>%command.name%</> command dumps the shell completion script required
to use shell autocompletion (currently only bash completion is supported).
<comment>Static installation
-------------------</>
Dump the script to a global completion file and restart your shell:
<info>%command.full_name% bash | sudo tee /etc/bash_completion.d/{$commandName}</>
Or dump the script to a local file and source it:
<info>%command.full_name% bash > completion.sh</>
<comment># source the file whenever you use the project</>
<info>source completion.sh</>
<comment># or add this line at the end of your "~/.bashrc" file:</>
<info>source /path/to/completion.sh</>
<comment>Dynamic installation
--------------------</>
Add this to the end of your shell configuration file (e.g. <info>"~/.bashrc"</>):
<info>eval "$({$fullCommand} completion bash)"</>
EOH
)
->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given')
->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$commandName = basename($_SERVER['argv'][0]);
if ($input->getOption('debug')) {
$this->tailDebugLog($commandName, $output);
return self::SUCCESS;
}
$shell = $input->getArgument('shell') ?? self::guessShell();
$completionFile = __DIR__.'/../Resources/completion.'.$shell;
if (!file_exists($completionFile)) {
$supportedShells = $this->getSupportedShells();
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
}
if ($shell) {
$output->writeln(sprintf('<error>Detected shell "%s", which is not supported by Symfony shell completion (supported shells: "%s").</>', $shell, implode('", "', $supportedShells)));
} else {
$output->writeln(sprintf('<error>Shell not detected, Symfony shell completion only supports "%s").</>', implode('", "', $supportedShells)));
}
return self::INVALID;
}
$output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, $this->getApplication()->getVersion()], file_get_contents($completionFile)));
return self::SUCCESS;
}
private static function guessShell(): string
{
return basename($_SERVER['SHELL'] ?? '');
}
private function tailDebugLog(string $commandName, OutputInterface $output): void
{
$debugFile = sys_get_temp_dir().'/sf_'.$commandName.'.log';
if (!file_exists($debugFile)) {
touch($debugFile);
}
$process = new Process(['tail', '-f', $debugFile], null, null, null, 0);
$process->run(function (string $type, string $line) use ($output): void {
$output->write($line);
});
}
/**
* @return string[]
*/
private function getSupportedShells(): array
{
return array_map(function ($f) {
return pathinfo($f, \PATHINFO_EXTENSION);
}, glob(__DIR__.'/../Resources/completion.*'));
}
}

View File

@@ -11,10 +11,13 @@
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Descriptor\ApplicationDescription;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
@@ -35,20 +38,20 @@ class HelpCommand extends Command
$this
->setName('help')
->setDefinition(array(
->setDefinition([
new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'),
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'),
))
->setDescription('Displays help for a command')
])
->setDescription('Display help for a command')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command displays help for a given command:
<info>php %command.full_name% list</info>
<info>%command.full_name% list</info>
You can also output the help in other formats by using the <comment>--format</comment> option:
<info>php %command.full_name% --format=xml list</info>
<info>%command.full_name% --format=xml list</info>
To display the list of available commands, please use the <info>list</info> command.
EOF
@@ -56,11 +59,6 @@ EOF
;
}
/**
* Sets the command.
*
* @param Command $command The command to set
*/
public function setCommand(Command $command)
{
$this->command = $command;
@@ -69,18 +67,33 @@ EOF
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (null === $this->command) {
$this->command = $this->getApplication()->find($input->getArgument('command_name'));
}
$this->command ??= $this->getApplication()->find($input->getArgument('command_name'));
$helper = new DescriptorHelper();
$helper->describe($output, $this->command, array(
$helper->describe($output, $this->command, [
'format' => $input->getOption('format'),
'raw_text' => $input->getOption('raw'),
));
]);
$this->command = null;
unset($this->command);
return 0;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('command_name')) {
$descriptor = new ApplicationDescription($this->getApplication());
$suggestions->suggestValues(array_keys($descriptor->getCommands()));
return;
}
if ($input->mustSuggestOptionValuesFor('format')) {
$helper = new DescriptorHelper();
$suggestions->suggestValues($helper->getFormats());
}
}
}

View File

@@ -0,0 +1,194 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
final class LazyCommand extends Command
{
private $command;
private ?bool $isEnabled;
public function __construct(string $name, array $aliases, string $description, bool $isHidden, \Closure $commandFactory, ?bool $isEnabled = true)
{
$this->setName($name)
->setAliases($aliases)
->setHidden($isHidden)
->setDescription($description);
$this->command = $commandFactory;
$this->isEnabled = $isEnabled;
}
public function ignoreValidationErrors(): void
{
$this->getCommand()->ignoreValidationErrors();
}
public function setApplication(Application $application = null): void
{
if ($this->command instanceof parent) {
$this->command->setApplication($application);
}
parent::setApplication($application);
}
public function setHelperSet(HelperSet $helperSet): void
{
if ($this->command instanceof parent) {
$this->command->setHelperSet($helperSet);
}
parent::setHelperSet($helperSet);
}
public function isEnabled(): bool
{
return $this->isEnabled ?? $this->getCommand()->isEnabled();
}
public function run(InputInterface $input, OutputInterface $output): int
{
return $this->getCommand()->run($input, $output);
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
$this->getCommand()->complete($input, $suggestions);
}
public function setCode(callable $code): static
{
$this->getCommand()->setCode($code);
return $this;
}
/**
* @internal
*/
public function mergeApplicationDefinition(bool $mergeArgs = true): void
{
$this->getCommand()->mergeApplicationDefinition($mergeArgs);
}
public function setDefinition(array|InputDefinition $definition): static
{
$this->getCommand()->setDefinition($definition);
return $this;
}
public function getDefinition(): InputDefinition
{
return $this->getCommand()->getDefinition();
}
public function getNativeDefinition(): InputDefinition
{
return $this->getCommand()->getNativeDefinition();
}
public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null): static
{
$this->getCommand()->addArgument($name, $mode, $description, $default);
return $this;
}
public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null): static
{
$this->getCommand()->addOption($name, $shortcut, $mode, $description, $default);
return $this;
}
public function setProcessTitle(string $title): static
{
$this->getCommand()->setProcessTitle($title);
return $this;
}
public function setHelp(string $help): static
{
$this->getCommand()->setHelp($help);
return $this;
}
public function getHelp(): string
{
return $this->getCommand()->getHelp();
}
public function getProcessedHelp(): string
{
return $this->getCommand()->getProcessedHelp();
}
public function getSynopsis(bool $short = false): string
{
return $this->getCommand()->getSynopsis($short);
}
public function addUsage(string $usage): static
{
$this->getCommand()->addUsage($usage);
return $this;
}
public function getUsages(): array
{
return $this->getCommand()->getUsages();
}
public function getHelper(string $name): mixed
{
return $this->getCommand()->getHelper($name);
}
public function getCommand(): parent
{
if (!$this->command instanceof \Closure) {
return $this->command;
}
$command = $this->command = ($this->command)();
$command->setApplication($this->getApplication());
if (null !== $this->getHelperSet()) {
$command->setHelperSet($this->getHelperSet());
}
$command->setName($this->getName())
->setAliases($this->getAliases())
->setHidden($this->isHidden())
->setDescription($this->getDescription());
// Will throw if the command is not correctly initialized.
$command->getDefinition();
return $command;
}
}

View File

@@ -11,12 +11,14 @@
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Descriptor\ApplicationDescription;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputDefinition;
/**
* ListCommand displays the list of all available commands for the application.
@@ -32,24 +34,29 @@ class ListCommand extends Command
{
$this
->setName('list')
->setDefinition($this->createDefinition())
->setDescription('Lists commands')
->setDefinition([
new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'),
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'),
new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'),
])
->setDescription('List commands')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command lists all commands:
<info>php %command.full_name%</info>
<info>%command.full_name%</info>
You can also display the commands for a specific namespace:
<info>php %command.full_name% test</info>
<info>%command.full_name% test</info>
You can also output the information in other formats by using the <comment>--format</comment> option:
<info>php %command.full_name% --format=xml</info>
<info>%command.full_name% --format=xml</info>
It's also possible to get raw list of commands (useful for embedding command runner):
<info>php %command.full_name% --raw</info>
<info>%command.full_name% --raw</info>
EOF
)
;
@@ -58,33 +65,31 @@ EOF
/**
* {@inheritdoc}
*/
public function getNativeDefinition()
{
return $this->createDefinition();
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
protected function execute(InputInterface $input, OutputInterface $output): int
{
$helper = new DescriptorHelper();
$helper->describe($output, $this->getApplication(), array(
$helper->describe($output, $this->getApplication(), [
'format' => $input->getOption('format'),
'raw_text' => $input->getOption('raw'),
'namespace' => $input->getArgument('namespace'),
));
'short' => $input->getOption('short'),
]);
return 0;
}
/**
* {@inheritdoc}
*/
private function createDefinition()
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
return new InputDefinition(array(
new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'),
new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'),
new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'),
));
if ($input->mustSuggestArgumentValuesFor('namespace')) {
$descriptor = new ApplicationDescription($this->getApplication());
$suggestions->suggestValues(array_keys($descriptor->getNamespaces()));
return;
}
if ($input->mustSuggestOptionValuesFor('format')) {
$helper = new DescriptorHelper();
$suggestions->suggestValues($helper->getFormats());
}
}
}

View File

@@ -12,8 +12,9 @@
namespace Symfony\Component\Console\Command;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Filesystem\LockHandler;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\Store\FlockStore;
use Symfony\Component\Lock\Store\SemaphoreStore;
/**
* Basic lock feature for commands.
@@ -22,27 +23,30 @@ use Symfony\Component\Filesystem\LockHandler;
*/
trait LockableTrait
{
private $lockHandler;
private $lock = null;
/**
* Locks a command.
*
* @return bool
*/
private function lock($name = null, $blocking = false)
private function lock(string $name = null, bool $blocking = false): bool
{
if (!class_exists(LockHandler::class)) {
throw new RuntimeException('To enable the locking feature you must install the symfony/filesystem component.');
if (!class_exists(SemaphoreStore::class)) {
throw new LogicException('To enable the locking feature you must install the symfony/lock component.');
}
if (null !== $this->lockHandler) {
if (null !== $this->lock) {
throw new LogicException('A lock is already in place.');
}
$this->lockHandler = new LockHandler($name ?: $this->getName());
if (SemaphoreStore::isSupported()) {
$store = new SemaphoreStore();
} else {
$store = new FlockStore();
}
if (!$this->lockHandler->lock($blocking)) {
$this->lockHandler = null;
$this->lock = (new LockFactory($store))->createLock($name ?: $this->getName());
if (!$this->lock->acquire($blocking)) {
$this->lock = null;
return false;
}
@@ -55,9 +59,9 @@ trait LockableTrait
*/
private function release()
{
if ($this->lockHandler) {
$this->lockHandler->release();
$this->lockHandler = null;
if ($this->lock) {
$this->lock->release();
$this->lock = null;
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Console\Command;
/**
* Interface for command reacting to signal.
*
* @author Grégoire Pineau <lyrixx@lyrix.info>
*/
interface SignalableCommandInterface
{
/**
* Returns the list of signals to subscribe.
*/
public function getSubscribedSignals(): array;
/**
* The method will be called when the application is signaled.
*/
public function handleSignal(int $signal): void;
}