Dep: update
주로 PHAN
This commit is contained in:
+97
-33
@@ -13,15 +13,19 @@ namespace Symfony\Component\Console;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
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\Event\ConsoleCommandEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleErrorEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleSignalEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
|
||||
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Helper\DebugFormatterHelper;
|
||||
use Symfony\Component\Console\Helper\FormatterHelper;
|
||||
@@ -39,6 +43,7 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\SignalRegistry\SignalRegistry;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\ErrorHandler\ErrorHandler;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
@@ -76,6 +81,8 @@ class Application implements ResetInterface
|
||||
private $defaultCommand;
|
||||
private $singleCommand = false;
|
||||
private $initialized;
|
||||
private $signalRegistry;
|
||||
private $signalsToDispatchEvent = [];
|
||||
|
||||
public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
|
||||
{
|
||||
@@ -83,6 +90,10 @@ class Application implements ResetInterface
|
||||
$this->version = $version;
|
||||
$this->terminal = new Terminal();
|
||||
$this->defaultCommand = 'list';
|
||||
if (\defined('SIGINT') && SignalRegistry::isSupported()) {
|
||||
$this->signalRegistry = new SignalRegistry();
|
||||
$this->signalsToDispatchEvent = [\SIGINT, \SIGTERM, \SIGUSR1, \SIGUSR2];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,6 +109,20 @@ class Application implements ResetInterface
|
||||
$this->commandLoader = $commandLoader;
|
||||
}
|
||||
|
||||
public function getSignalRegistry(): SignalRegistry
|
||||
{
|
||||
if (!$this->signalRegistry) {
|
||||
throw new RuntimeException('Signals are not supported. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
|
||||
}
|
||||
|
||||
return $this->signalRegistry;
|
||||
}
|
||||
|
||||
public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent)
|
||||
{
|
||||
$this->signalsToDispatchEvent = $signalsToDispatchEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the current application.
|
||||
*
|
||||
@@ -107,8 +132,10 @@ class Application implements ResetInterface
|
||||
*/
|
||||
public function run(InputInterface $input = null, OutputInterface $output = null)
|
||||
{
|
||||
putenv('LINES='.$this->terminal->getHeight());
|
||||
putenv('COLUMNS='.$this->terminal->getWidth());
|
||||
if (\function_exists('putenv')) {
|
||||
@putenv('LINES='.$this->terminal->getHeight());
|
||||
@putenv('COLUMNS='.$this->terminal->getWidth());
|
||||
}
|
||||
|
||||
if (null === $input) {
|
||||
$input = new ArgvInput();
|
||||
@@ -260,6 +287,10 @@ class Application implements ResetInterface
|
||||
$command = $this->find($alternative);
|
||||
}
|
||||
|
||||
if ($command instanceof LazyCommand) {
|
||||
$command = $command->getCommand();
|
||||
}
|
||||
|
||||
$this->runningCommand = $command;
|
||||
$exitCode = $this->doRunCommand($command, $input, $output);
|
||||
$this->runningCommand = null;
|
||||
@@ -463,11 +494,13 @@ class Application implements ResetInterface
|
||||
return null;
|
||||
}
|
||||
|
||||
// Will throw if the command is not correctly initialized.
|
||||
$command->getDefinition();
|
||||
if (!$command instanceof LazyCommand) {
|
||||
// Will throw if the command is not correctly initialized.
|
||||
$command->getDefinition();
|
||||
}
|
||||
|
||||
if (!$command->getName()) {
|
||||
throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', \get_class($command)));
|
||||
throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command)));
|
||||
}
|
||||
|
||||
$this->commands[$command->getName()] = $command;
|
||||
@@ -494,6 +527,11 @@ class Application implements ResetInterface
|
||||
throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name));
|
||||
}
|
||||
|
||||
// When the command has a different name than the one used at the command loader level
|
||||
if (!isset($this->commands[$name])) {
|
||||
throw new CommandNotFoundException(sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name));
|
||||
}
|
||||
|
||||
$command = $this->commands[$name];
|
||||
|
||||
if ($this->wantHelps) {
|
||||
@@ -555,7 +593,7 @@ class Application implements ResetInterface
|
||||
public function findNamespace(string $namespace)
|
||||
{
|
||||
$allNamespaces = $this->getNamespaces();
|
||||
$expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $namespace);
|
||||
$expr = implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*';
|
||||
$namespaces = preg_grep('{^'.$expr.'}', $allNamespaces);
|
||||
|
||||
if (empty($namespaces)) {
|
||||
@@ -611,7 +649,7 @@ class Application implements ResetInterface
|
||||
}
|
||||
|
||||
$allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands);
|
||||
$expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name);
|
||||
$expr = implode('[^:]*:', array_map('preg_quote', explode(':', $name))).'[^:]*';
|
||||
$commands = preg_grep('{^'.$expr.'}', $allCommands);
|
||||
|
||||
if (empty($commands)) {
|
||||
@@ -665,7 +703,7 @@ class Application implements ResetInterface
|
||||
$abbrevs = array_values($commands);
|
||||
$maxLen = 0;
|
||||
foreach ($abbrevs as $abbrev) {
|
||||
$maxLen = max(Helper::strlen($abbrev), $maxLen);
|
||||
$maxLen = max(Helper::width($abbrev), $maxLen);
|
||||
}
|
||||
$abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) {
|
||||
if ($commandList[$cmd]->isHidden()) {
|
||||
@@ -676,7 +714,7 @@ class Application implements ResetInterface
|
||||
|
||||
$abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription();
|
||||
|
||||
return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
|
||||
return Helper::width($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev;
|
||||
}, array_values($commands));
|
||||
|
||||
if (\count($commands) > 1) {
|
||||
@@ -764,7 +802,7 @@ class Application implements ResetInterface
|
||||
$this->doRenderThrowable($e, $output);
|
||||
|
||||
if (null !== $this->runningCommand) {
|
||||
$output->writeln(sprintf('<info>%s</info>', sprintf($this->runningCommand->getSynopsis(), $this->getName())), OutputInterface::VERBOSITY_QUIET);
|
||||
$output->writeln(sprintf('<info>%s</info>', OutputFormatter::escape(sprintf($this->runningCommand->getSynopsis(), $this->getName()))), OutputInterface::VERBOSITY_QUIET);
|
||||
$output->writeln('', OutputInterface::VERBOSITY_QUIET);
|
||||
}
|
||||
}
|
||||
@@ -774,26 +812,25 @@ class Application implements ResetInterface
|
||||
do {
|
||||
$message = trim($e->getMessage());
|
||||
if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
|
||||
$class = \get_class($e);
|
||||
$class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
|
||||
$class = get_debug_type($e);
|
||||
$title = sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : '');
|
||||
$len = Helper::strlen($title);
|
||||
$len = Helper::width($title);
|
||||
} else {
|
||||
$len = 0;
|
||||
}
|
||||
|
||||
if (false !== strpos($message, "class@anonymous\0")) {
|
||||
$message = preg_replace_callback('/class@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
|
||||
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
|
||||
if (str_contains($message, "@anonymous\0")) {
|
||||
$message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) {
|
||||
return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
|
||||
}, $message);
|
||||
}
|
||||
|
||||
$width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : PHP_INT_MAX;
|
||||
$width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX;
|
||||
$lines = [];
|
||||
foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) {
|
||||
foreach ($this->splitStringByWidth($line, $width - 4) as $line) {
|
||||
// pre-format lines to get the right string length
|
||||
$lineLength = Helper::strlen($line) + 4;
|
||||
$lineLength = Helper::width($line) + 4;
|
||||
$lines[] = [$line, $lineLength];
|
||||
|
||||
$len = max($lineLength, $len);
|
||||
@@ -806,7 +843,7 @@ class Application implements ResetInterface
|
||||
}
|
||||
$messages[] = $emptyLine = sprintf('<error>%s</error>', str_repeat(' ', $len));
|
||||
if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
|
||||
$messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - Helper::strlen($title))));
|
||||
$messages[] = sprintf('<error>%s%s</error>', $title, str_repeat(' ', max(0, $len - Helper::width($title))));
|
||||
}
|
||||
foreach ($lines as $line) {
|
||||
$messages[] = sprintf('<error> %s %s</error>', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1]));
|
||||
@@ -830,11 +867,11 @@ class Application implements ResetInterface
|
||||
]);
|
||||
|
||||
for ($i = 0, $count = \count($trace); $i < $count; ++$i) {
|
||||
$class = isset($trace[$i]['class']) ? $trace[$i]['class'] : '';
|
||||
$type = isset($trace[$i]['type']) ? $trace[$i]['type'] : '';
|
||||
$function = isset($trace[$i]['function']) ? $trace[$i]['function'] : '';
|
||||
$file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a';
|
||||
$line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a';
|
||||
$class = $trace[$i]['class'] ?? '';
|
||||
$type = $trace[$i]['type'] ?? '';
|
||||
$function = $trace[$i]['function'] ?? '';
|
||||
$file = $trace[$i]['file'] ?? 'n/a';
|
||||
$line = $trace[$i]['line'] ?? 'n/a';
|
||||
|
||||
$output->writeln(sprintf(' %s%s at <info>%s:%s</info>', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET);
|
||||
}
|
||||
@@ -887,7 +924,9 @@ class Application implements ResetInterface
|
||||
$input->setInteractive(false);
|
||||
}
|
||||
|
||||
putenv('SHELL_VERBOSITY='.$shellVerbosity);
|
||||
if (\function_exists('putenv')) {
|
||||
@putenv('SHELL_VERBOSITY='.$shellVerbosity);
|
||||
}
|
||||
$_ENV['SHELL_VERBOSITY'] = $shellVerbosity;
|
||||
$_SERVER['SHELL_VERBOSITY'] = $shellVerbosity;
|
||||
}
|
||||
@@ -908,6 +947,33 @@ class Application implements ResetInterface
|
||||
}
|
||||
}
|
||||
|
||||
if ($command instanceof SignalableCommandInterface && ($this->signalsToDispatchEvent || $command->getSubscribedSignals())) {
|
||||
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 ($this->dispatcher) {
|
||||
foreach ($this->signalsToDispatchEvent as $signal) {
|
||||
$event = new ConsoleSignalEvent($command, $input, $output, $signal);
|
||||
|
||||
$this->signalRegistry->register($signal, function ($signal, $hasNext) use ($event) {
|
||||
$this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
|
||||
|
||||
// No more handlers, we try to simulate PHP default behavior
|
||||
if (!$hasNext) {
|
||||
if (!\in_array($signal, [\SIGUSR1, \SIGUSR2], true)) {
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($command->getSubscribedSignals() as $signal) {
|
||||
$this->signalRegistry->register($signal, [$command, 'handleSignal']);
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $this->dispatcher) {
|
||||
return $command->run($input, $output);
|
||||
}
|
||||
@@ -970,13 +1036,11 @@ class Application implements ResetInterface
|
||||
{
|
||||
return new InputDefinition([
|
||||
new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'),
|
||||
|
||||
new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'),
|
||||
new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display help for the given command. When no command is given display help for the <info>'.$this->defaultCommand.'</info> command'),
|
||||
new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'),
|
||||
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_NONE, 'Force ANSI output'),
|
||||
new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'),
|
||||
new InputOption('--ansi', '', InputOption::VALUE_NEGATABLE, 'Force (or disable --no-ansi) ANSI output', false),
|
||||
new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'),
|
||||
]);
|
||||
}
|
||||
@@ -1055,7 +1119,7 @@ class Application implements ResetInterface
|
||||
}
|
||||
|
||||
$lev = levenshtein($subname, $parts[$i]);
|
||||
if ($lev <= \strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) {
|
||||
if ($lev <= \strlen($subname) / 3 || '' !== $subname && str_contains($parts[$i], $subname)) {
|
||||
$alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev;
|
||||
} elseif ($exists) {
|
||||
$alternatives[$collectionName] += $threshold;
|
||||
@@ -1065,13 +1129,13 @@ class Application implements ResetInterface
|
||||
|
||||
foreach ($collection as $item) {
|
||||
$lev = levenshtein($name, $item);
|
||||
if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) {
|
||||
if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) {
|
||||
$alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev;
|
||||
}
|
||||
}
|
||||
|
||||
$alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; });
|
||||
ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE);
|
||||
|
||||
return array_keys($alternatives);
|
||||
}
|
||||
@@ -1083,7 +1147,7 @@ class Application implements ResetInterface
|
||||
*/
|
||||
public function setDefaultCommand(string $commandName, bool $isSingleCommand = false)
|
||||
{
|
||||
$this->defaultCommand = $commandName;
|
||||
$this->defaultCommand = explode('|', ltrim($commandName, '|'))[0];
|
||||
|
||||
if ($isSingleCommand) {
|
||||
// Ensure the command exist
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?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\Attribute;
|
||||
|
||||
/**
|
||||
* Service tag to autoconfigure commands.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS)]
|
||||
class AsCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?string $description = null,
|
||||
array $aliases = [],
|
||||
bool $hidden = false,
|
||||
) {
|
||||
if (!$hidden && !$aliases) {
|
||||
return;
|
||||
}
|
||||
|
||||
$name = explode('|', $name);
|
||||
$name = array_merge($name, $aliases);
|
||||
|
||||
if ($hidden && '' !== $name[0]) {
|
||||
array_unshift($name, '');
|
||||
}
|
||||
|
||||
$this->name = implode('|', $name);
|
||||
}
|
||||
}
|
||||
Vendored
+48
-16
@@ -1,6 +1,38 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
5.3
|
||||
---
|
||||
|
||||
* Add `GithubActionReporter` to render annotations in a Github Action
|
||||
* Add `InputOption::VALUE_NEGATABLE` flag to handle `--foo`/`--no-foo` options
|
||||
* Add the `Command::$defaultDescription` static property and the `description` attribute
|
||||
on the `console.command` tag to allow the `list` command to instantiate commands lazily
|
||||
* Add option `--short` to the `list` command
|
||||
* Add support for bright colors
|
||||
* Add `#[AsCommand]` attribute for declaring commands on PHP 8
|
||||
* Add `Helper::width()` and `Helper::length()`
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
||||
* Added `SingleCommandApplication::setAutoExit()` to allow testing via `CommandTester`
|
||||
* added support for multiline responses to questions through `Question::setMultiline()`
|
||||
and `Question::isMultiline()`
|
||||
* Added `SignalRegistry` class to stack signals handlers
|
||||
* Added support for signals:
|
||||
* Added `Application::getSignalRegistry()` and `Application::setSignalsToDispatchEvent()` methods
|
||||
* Added `SignalableCommandInterface` interface
|
||||
* Added `TableCellStyle` class to customize table cell
|
||||
* Removed `php ` prefix invocation from help messages.
|
||||
|
||||
5.1.0
|
||||
-----
|
||||
|
||||
* `Command::setHidden()` is final since Symfony 5.1
|
||||
* Add `SingleCommandApplication`
|
||||
* Add `Cursor` class
|
||||
|
||||
5.0.0
|
||||
-----
|
||||
|
||||
@@ -63,9 +95,9 @@ CHANGELOG
|
||||
* `OutputFormatter` throws an exception when unknown options are used
|
||||
* removed `QuestionHelper::setInputStream()/getInputStream()`
|
||||
* removed `Application::getTerminalWidth()/getTerminalHeight()` and
|
||||
`Application::setTerminalDimensions()/getTerminalDimensions()`
|
||||
* removed `ConsoleExceptionEvent`
|
||||
* removed `ConsoleEvents::EXCEPTION`
|
||||
`Application::setTerminalDimensions()/getTerminalDimensions()`
|
||||
* removed `ConsoleExceptionEvent`
|
||||
* removed `ConsoleEvents::EXCEPTION`
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
@@ -82,23 +114,23 @@ CHANGELOG
|
||||
3.3.0
|
||||
-----
|
||||
|
||||
* added `ExceptionListener`
|
||||
* added `AddConsoleCommandPass` (originally in FrameworkBundle)
|
||||
* [BC BREAK] `Input::getOption()` no longer returns the default value for options
|
||||
with value optional explicitly passed empty
|
||||
* added console.error event to catch exceptions thrown by other listeners
|
||||
* deprecated console.exception event in favor of console.error
|
||||
* added ability to handle `CommandNotFoundException` through the
|
||||
`console.error` event
|
||||
* deprecated default validation in `SymfonyQuestionHelper::ask`
|
||||
* added `ExceptionListener`
|
||||
* added `AddConsoleCommandPass` (originally in FrameworkBundle)
|
||||
* [BC BREAK] `Input::getOption()` no longer returns the default value for options
|
||||
with value optional explicitly passed empty
|
||||
* added console.error event to catch exceptions thrown by other listeners
|
||||
* deprecated console.exception event in favor of console.error
|
||||
* added ability to handle `CommandNotFoundException` through the
|
||||
`console.error` event
|
||||
* deprecated default validation in `SymfonyQuestionHelper::ask`
|
||||
|
||||
3.2.0
|
||||
------
|
||||
|
||||
* added `setInputs()` method to CommandTester for ease testing of commands expecting inputs
|
||||
* added `setStream()` and `getStream()` methods to Input (implement StreamableInputInterface)
|
||||
* added StreamableInputInterface
|
||||
* added LockableTrait
|
||||
* added `setInputs()` method to CommandTester for ease testing of commands expecting inputs
|
||||
* added `setStream()` and `getStream()` methods to Input (implement StreamableInputInterface)
|
||||
* added StreamableInputInterface
|
||||
* added LockableTrait
|
||||
|
||||
3.1.0
|
||||
-----
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?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\CI;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Utility class for Github actions.
|
||||
*
|
||||
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
|
||||
*/
|
||||
class GithubActionReporter
|
||||
{
|
||||
private $output;
|
||||
|
||||
/**
|
||||
* @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L80-L85
|
||||
*/
|
||||
private const ESCAPED_DATA = [
|
||||
'%' => '%25',
|
||||
"\r" => '%0D',
|
||||
"\n" => '%0A',
|
||||
];
|
||||
|
||||
/**
|
||||
* @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L87-L94
|
||||
*/
|
||||
private const ESCAPED_PROPERTIES = [
|
||||
'%' => '%25',
|
||||
"\r" => '%0D',
|
||||
"\n" => '%0A',
|
||||
':' => '%3A',
|
||||
',' => '%2C',
|
||||
];
|
||||
|
||||
public function __construct(OutputInterface $output)
|
||||
{
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
public static function isGithubActionEnvironment(): bool
|
||||
{
|
||||
return false !== getenv('GITHUB_ACTIONS');
|
||||
}
|
||||
|
||||
/**
|
||||
* Output an error using the Github annotations format.
|
||||
*
|
||||
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
|
||||
*/
|
||||
public function error(string $message, string $file = null, int $line = null, int $col = null): void
|
||||
{
|
||||
$this->log('error', $message, $file, $line, $col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a warning using the Github annotations format.
|
||||
*
|
||||
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message
|
||||
*/
|
||||
public function warning(string $message, string $file = null, int $line = null, int $col = null): void
|
||||
{
|
||||
$this->log('warning', $message, $file, $line, $col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output a debug log using the Github annotations format.
|
||||
*
|
||||
* @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message
|
||||
*/
|
||||
public function debug(string $message, string $file = null, int $line = null, int $col = null): void
|
||||
{
|
||||
$this->log('debug', $message, $file, $line, $col);
|
||||
}
|
||||
|
||||
private function log(string $type, string $message, string $file = null, int $line = null, int $col = null): void
|
||||
{
|
||||
// Some values must be encoded.
|
||||
$message = strtr($message, self::ESCAPED_DATA);
|
||||
|
||||
if (!$file) {
|
||||
// No file provided, output the message solely:
|
||||
$this->output->writeln(sprintf('::%s::%s', $type, $message));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output->writeln(sprintf('::%s file=%s,line=%s,col=%s::%s', $type, strtr($file, self::ESCAPED_PROPERTIES), strtr($line ?? 1, self::ESCAPED_PROPERTIES), strtr($col ?? 0, self::ESCAPED_PROPERTIES), $message));
|
||||
}
|
||||
}
|
||||
Vendored
+180
@@ -0,0 +1,180 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
final class Color
|
||||
{
|
||||
private const COLORS = [
|
||||
'black' => 0,
|
||||
'red' => 1,
|
||||
'green' => 2,
|
||||
'yellow' => 3,
|
||||
'blue' => 4,
|
||||
'magenta' => 5,
|
||||
'cyan' => 6,
|
||||
'white' => 7,
|
||||
'default' => 9,
|
||||
];
|
||||
|
||||
private const BRIGHT_COLORS = [
|
||||
'gray' => 0,
|
||||
'bright-red' => 1,
|
||||
'bright-green' => 2,
|
||||
'bright-yellow' => 3,
|
||||
'bright-blue' => 4,
|
||||
'bright-magenta' => 5,
|
||||
'bright-cyan' => 6,
|
||||
'bright-white' => 7,
|
||||
];
|
||||
|
||||
private const AVAILABLE_OPTIONS = [
|
||||
'bold' => ['set' => 1, 'unset' => 22],
|
||||
'underscore' => ['set' => 4, 'unset' => 24],
|
||||
'blink' => ['set' => 5, 'unset' => 25],
|
||||
'reverse' => ['set' => 7, 'unset' => 27],
|
||||
'conceal' => ['set' => 8, 'unset' => 28],
|
||||
];
|
||||
|
||||
private $foreground;
|
||||
private $background;
|
||||
private $options = [];
|
||||
|
||||
public function __construct(string $foreground = '', string $background = '', array $options = [])
|
||||
{
|
||||
$this->foreground = $this->parseColor($foreground);
|
||||
$this->background = $this->parseColor($background, true);
|
||||
|
||||
foreach ($options as $option) {
|
||||
if (!isset(self::AVAILABLE_OPTIONS[$option])) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS))));
|
||||
}
|
||||
|
||||
$this->options[$option] = self::AVAILABLE_OPTIONS[$option];
|
||||
}
|
||||
}
|
||||
|
||||
public function apply(string $text): string
|
||||
{
|
||||
return $this->set().$text.$this->unset();
|
||||
}
|
||||
|
||||
public function set(): string
|
||||
{
|
||||
$setCodes = [];
|
||||
if ('' !== $this->foreground) {
|
||||
$setCodes[] = $this->foreground;
|
||||
}
|
||||
if ('' !== $this->background) {
|
||||
$setCodes[] = $this->background;
|
||||
}
|
||||
foreach ($this->options as $option) {
|
||||
$setCodes[] = $option['set'];
|
||||
}
|
||||
if (0 === \count($setCodes)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf("\033[%sm", implode(';', $setCodes));
|
||||
}
|
||||
|
||||
public function unset(): string
|
||||
{
|
||||
$unsetCodes = [];
|
||||
if ('' !== $this->foreground) {
|
||||
$unsetCodes[] = 39;
|
||||
}
|
||||
if ('' !== $this->background) {
|
||||
$unsetCodes[] = 49;
|
||||
}
|
||||
foreach ($this->options as $option) {
|
||||
$unsetCodes[] = $option['unset'];
|
||||
}
|
||||
if (0 === \count($unsetCodes)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf("\033[%sm", implode(';', $unsetCodes));
|
||||
}
|
||||
|
||||
private function parseColor(string $color, bool $background = false): string
|
||||
{
|
||||
if ('' === $color) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ('#' === $color[0]) {
|
||||
$color = substr($color, 1);
|
||||
|
||||
if (3 === \strlen($color)) {
|
||||
$color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2];
|
||||
}
|
||||
|
||||
if (6 !== \strlen($color)) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid "%s" color.', $color));
|
||||
}
|
||||
|
||||
return ($background ? '4' : '3').$this->convertHexColorToAnsi(hexdec($color));
|
||||
}
|
||||
|
||||
if (isset(self::COLORS[$color])) {
|
||||
return ($background ? '4' : '3').self::COLORS[$color];
|
||||
}
|
||||
|
||||
if (isset(self::BRIGHT_COLORS[$color])) {
|
||||
return ($background ? '10' : '9').self::BRIGHT_COLORS[$color];
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS)))));
|
||||
}
|
||||
|
||||
private function convertHexColorToAnsi(int $color): string
|
||||
{
|
||||
$r = ($color >> 16) & 255;
|
||||
$g = ($color >> 8) & 255;
|
||||
$b = $color & 255;
|
||||
|
||||
// see https://github.com/termstandard/colors/ for more information about true color support
|
||||
if ('truecolor' !== getenv('COLORTERM')) {
|
||||
return (string) $this->degradeHexColorToAnsi($r, $g, $b);
|
||||
}
|
||||
|
||||
return sprintf('8;2;%d;%d;%d', $r, $g, $b);
|
||||
}
|
||||
|
||||
private function degradeHexColorToAnsi(int $r, int $g, int $b): int
|
||||
{
|
||||
if (0 === round($this->getSaturation($r, $g, $b) / 50)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (round($b / 255) << 2) | (round($g / 255) << 1) | round($r / 255);
|
||||
}
|
||||
|
||||
private function getSaturation(int $r, int $g, int $b): int
|
||||
{
|
||||
$r = $r / 255;
|
||||
$g = $g / 255;
|
||||
$b = $b / 255;
|
||||
$v = max($r, $g, $b);
|
||||
|
||||
if (0 === $diff = $v - min($r, $g, $b)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) $diff * 100 / $v;
|
||||
}
|
||||
}
|
||||
+100
-38
@@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
@@ -29,11 +30,21 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
*/
|
||||
class Command
|
||||
{
|
||||
// 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;
|
||||
private $name;
|
||||
private $processTitle;
|
||||
@@ -42,9 +53,8 @@ class Command
|
||||
private $hidden = false;
|
||||
private $help = '';
|
||||
private $description = '';
|
||||
private $fullDefinition;
|
||||
private $ignoreValidationErrors = false;
|
||||
private $applicationDefinitionMerged = false;
|
||||
private $applicationDefinitionMergedWithArgs = false;
|
||||
private $code;
|
||||
private $synopsis = [];
|
||||
private $usages = [];
|
||||
@@ -56,11 +66,32 @@ class Command
|
||||
public static function getDefaultName()
|
||||
{
|
||||
$class = static::class;
|
||||
|
||||
if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
|
||||
return $attribute[0]->newInstance()->name;
|
||||
}
|
||||
|
||||
$r = new \ReflectionProperty($class, 'defaultName');
|
||||
|
||||
return $class === $r->class ? static::$defaultName : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null The default command description or null when no default description is set
|
||||
*/
|
||||
public static function getDefaultDescription(): ?string
|
||||
{
|
||||
$class = static::class;
|
||||
|
||||
if (\PHP_VERSION_ID >= 80000 && $attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) {
|
||||
return $attribute[0]->newInstance()->description;
|
||||
}
|
||||
|
||||
$r = new \ReflectionProperty($class, 'defaultDescription');
|
||||
|
||||
return $class === $r->class ? static::$defaultDescription : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $name The name of the command; passing null means it must be set in configure()
|
||||
*
|
||||
@@ -70,10 +101,25 @@ class Command
|
||||
{
|
||||
$this->definition = new InputDefinition();
|
||||
|
||||
if (null !== $name || null !== $name = static::getDefaultName()) {
|
||||
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);
|
||||
}
|
||||
|
||||
if ('' === $this->description) {
|
||||
$this->setDescription(static::getDefaultDescription() ?? '');
|
||||
}
|
||||
|
||||
$this->configure();
|
||||
}
|
||||
|
||||
@@ -95,6 +141,8 @@ class Command
|
||||
} else {
|
||||
$this->helperSet = null;
|
||||
}
|
||||
|
||||
$this->fullDefinition = null;
|
||||
}
|
||||
|
||||
public function setHelperSet(HelperSet $helperSet)
|
||||
@@ -202,16 +250,12 @@ class Command
|
||||
*/
|
||||
public function run(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
// 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;
|
||||
@@ -223,7 +267,7 @@ class Command
|
||||
if (null !== $this->processTitle) {
|
||||
if (\function_exists('cli_set_process_title')) {
|
||||
if (!@cli_set_process_title($this->processTitle)) {
|
||||
if ('Darwin' === PHP_OS) {
|
||||
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 {
|
||||
cli_set_process_title($this->processTitle);
|
||||
@@ -255,7 +299,7 @@ class Command
|
||||
$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, \gettype($statusCode)));
|
||||
throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +325,14 @@ class Command
|
||||
if ($code instanceof \Closure) {
|
||||
$r = new \ReflectionFunction($code);
|
||||
if (null === $r->getClosureThis()) {
|
||||
$code = \Closure::bind($code, $this);
|
||||
set_error_handler(static function () {});
|
||||
try {
|
||||
if ($c = \Closure::bind($code, $this)) {
|
||||
$code = $c;
|
||||
}
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,23 +347,24 @@ 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(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->applicationDefinitionMerged = true;
|
||||
$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->applicationDefinitionMergedWithArgs = true;
|
||||
$this->fullDefinition->setArguments($this->application->getDefinition()->getArguments());
|
||||
$this->fullDefinition->addArguments($this->definition->getArguments());
|
||||
} else {
|
||||
$this->fullDefinition->setArguments($this->definition->getArguments());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,7 +383,7 @@ class Command
|
||||
$this->definition->setDefinition($definition);
|
||||
}
|
||||
|
||||
$this->applicationDefinitionMerged = false;
|
||||
$this->fullDefinition = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -343,11 +395,7 @@ class Command
|
||||
*/
|
||||
public function getDefinition()
|
||||
{
|
||||
if (null === $this->definition) {
|
||||
throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
|
||||
}
|
||||
|
||||
return $this->definition;
|
||||
return $this->fullDefinition ?? $this->getNativeDefinition();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -362,14 +410,18 @@ class Command
|
||||
*/
|
||||
public function getNativeDefinition()
|
||||
{
|
||||
return $this->getDefinition();
|
||||
if (null === $this->definition) {
|
||||
throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
|
||||
}
|
||||
|
||||
return $this->definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an argument.
|
||||
*
|
||||
* @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL
|
||||
* @param string|string[]|null $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
|
||||
*
|
||||
@@ -378,6 +430,9 @@ class Command
|
||||
public function addArgument(string $name, int $mode = null, string $description = '', $default = null)
|
||||
{
|
||||
$this->definition->addArgument(new InputArgument($name, $mode, $description, $default));
|
||||
if (null !== $this->fullDefinition) {
|
||||
$this->fullDefinition->addArgument(new InputArgument($name, $mode, $description, $default));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -385,9 +440,9 @@ class Command
|
||||
/**
|
||||
* Adds an option.
|
||||
*
|
||||
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
|
||||
* @param int|null $mode The option mode: One of the InputOption::VALUE_* constants
|
||||
* @param string|string[]|int|bool|null $default The default value (must be null for InputOption::VALUE_NONE)
|
||||
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
|
||||
* @param int|null $mode The option mode: One of the InputOption::VALUE_* constants
|
||||
* @param mixed $default The default value (must be null for InputOption::VALUE_NONE)
|
||||
*
|
||||
* @throws InvalidArgumentException If option mode is invalid or incompatible
|
||||
*
|
||||
@@ -396,6 +451,9 @@ class Command
|
||||
public function addOption(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
@@ -427,8 +485,6 @@ 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
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setProcessTitle(string $title)
|
||||
@@ -450,10 +506,13 @@ class Command
|
||||
|
||||
/**
|
||||
* @param bool $hidden Whether or not the command should be hidden from the list of commands
|
||||
* The default value will be true in Symfony 6.0
|
||||
*
|
||||
* @return Command The current instance
|
||||
* @return $this
|
||||
*
|
||||
* @final since Symfony 5.1
|
||||
*/
|
||||
public function setHidden(bool $hidden)
|
||||
public function setHidden(bool $hidden /*= true*/)
|
||||
{
|
||||
$this->hidden = $hidden;
|
||||
|
||||
@@ -546,11 +605,14 @@ class Command
|
||||
*/
|
||||
public function setAliases(iterable $aliases)
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($aliases as $alias) {
|
||||
$this->validateName($alias);
|
||||
$list[] = $alias;
|
||||
}
|
||||
|
||||
$this->aliases = $aliases;
|
||||
$this->aliases = \is_array($aliases) ? $aliases : $list;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -590,7 +652,7 @@ class Command
|
||||
*/
|
||||
public function addUsage(string $usage)
|
||||
{
|
||||
if (0 !== strpos($usage, $this->name)) {
|
||||
if (!str_starts_with($usage, $this->name)) {
|
||||
$usage = sprintf('%s %s', $this->name, $usage);
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -40,15 +40,15 @@ class HelpCommand extends Command
|
||||
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
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
<?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\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 $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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setCode(callable $code): self
|
||||
{
|
||||
$this->getCommand()->setCode($code);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function mergeApplicationDefinition(bool $mergeArgs = true): void
|
||||
{
|
||||
$this->getCommand()->mergeApplicationDefinition($mergeArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setDefinition($definition): self
|
||||
{
|
||||
$this->getCommand()->setDefinition($definition);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDefinition(): InputDefinition
|
||||
{
|
||||
return $this->getCommand()->getDefinition();
|
||||
}
|
||||
|
||||
public function getNativeDefinition(): InputDefinition
|
||||
{
|
||||
return $this->getCommand()->getNativeDefinition();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function addArgument(string $name, int $mode = null, string $description = '', $default = null): self
|
||||
{
|
||||
$this->getCommand()->addArgument($name, $mode, $description, $default);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function addOption(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null): self
|
||||
{
|
||||
$this->getCommand()->addOption($name, $shortcut, $mode, $description, $default);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setProcessTitle(string $title): self
|
||||
{
|
||||
$this->getCommand()->setProcessTitle($title);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setHelp(string $help): self
|
||||
{
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function addUsage(string $usage): self
|
||||
{
|
||||
$this->getCommand()->addUsage($usage);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUsages(): array
|
||||
{
|
||||
return $this->getCommand()->getUsages();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getHelper(string $name)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
+12
-24
@@ -13,7 +13,6 @@ namespace Symfony\Component\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Helper\DescriptorHelper;
|
||||
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;
|
||||
@@ -32,37 +31,34 @@ 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
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNativeDefinition()
|
||||
{
|
||||
return $this->createDefinition();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -73,17 +69,9 @@ EOF
|
||||
'format' => $input->getOption('format'),
|
||||
'raw_text' => $input->getOption('raw'),
|
||||
'namespace' => $input->getArgument('namespace'),
|
||||
'short' => $input->getOption('short'),
|
||||
]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function createDefinition(): InputDefinition
|
||||
{
|
||||
return new InputDefinition([
|
||||
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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+29
-4
@@ -11,6 +11,11 @@
|
||||
|
||||
namespace Symfony\Component\Console;
|
||||
|
||||
use Symfony\Component\Console\Event\ConsoleCommandEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleErrorEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleSignalEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
|
||||
|
||||
/**
|
||||
* Contains all events dispatched by an Application.
|
||||
*
|
||||
@@ -21,11 +26,19 @@ final class ConsoleEvents
|
||||
/**
|
||||
* The COMMAND event allows you to attach listeners before any command is
|
||||
* executed by the console. It also allows you to modify the command, input and output
|
||||
* before they are handled to the command.
|
||||
* before they are handed to the command.
|
||||
*
|
||||
* @Event("Symfony\Component\Console\Event\ConsoleCommandEvent")
|
||||
*/
|
||||
const COMMAND = 'console.command';
|
||||
public const COMMAND = 'console.command';
|
||||
|
||||
/**
|
||||
* The SIGNAL event allows you to perform some actions
|
||||
* after the command execution was interrupted.
|
||||
*
|
||||
* @Event("Symfony\Component\Console\Event\ConsoleSignalEvent")
|
||||
*/
|
||||
public const SIGNAL = 'console.signal';
|
||||
|
||||
/**
|
||||
* The TERMINATE event allows you to attach listeners after a command is
|
||||
@@ -33,7 +46,7 @@ final class ConsoleEvents
|
||||
*
|
||||
* @Event("Symfony\Component\Console\Event\ConsoleTerminateEvent")
|
||||
*/
|
||||
const TERMINATE = 'console.terminate';
|
||||
public const TERMINATE = 'console.terminate';
|
||||
|
||||
/**
|
||||
* The ERROR event occurs when an uncaught exception or error appears.
|
||||
@@ -43,5 +56,17 @@ final class ConsoleEvents
|
||||
*
|
||||
* @Event("Symfony\Component\Console\Event\ConsoleErrorEvent")
|
||||
*/
|
||||
const ERROR = 'console.error';
|
||||
public const ERROR = 'console.error';
|
||||
|
||||
/**
|
||||
* Event aliases.
|
||||
*
|
||||
* These aliases can be consumed by RegisterListenersPass.
|
||||
*/
|
||||
public const ALIASES = [
|
||||
ConsoleCommandEvent::class => self::COMMAND,
|
||||
ConsoleErrorEvent::class => self::ERROR,
|
||||
ConsoleSignalEvent::class => self::SIGNAL,
|
||||
ConsoleTerminateEvent::class => self::TERMINATE,
|
||||
];
|
||||
}
|
||||
|
||||
Vendored
+168
@@ -0,0 +1,168 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Pierre du Plessis <pdples@gmail.com>
|
||||
*/
|
||||
final class Cursor
|
||||
{
|
||||
private $output;
|
||||
private $input;
|
||||
|
||||
public function __construct(OutputInterface $output, $input = null)
|
||||
{
|
||||
$this->output = $output;
|
||||
$this->input = $input ?? (\defined('STDIN') ? \STDIN : fopen('php://input', 'r+'));
|
||||
}
|
||||
|
||||
public function moveUp(int $lines = 1): self
|
||||
{
|
||||
$this->output->write(sprintf("\x1b[%dA", $lines));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function moveDown(int $lines = 1): self
|
||||
{
|
||||
$this->output->write(sprintf("\x1b[%dB", $lines));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function moveRight(int $columns = 1): self
|
||||
{
|
||||
$this->output->write(sprintf("\x1b[%dC", $columns));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function moveLeft(int $columns = 1): self
|
||||
{
|
||||
$this->output->write(sprintf("\x1b[%dD", $columns));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function moveToColumn(int $column): self
|
||||
{
|
||||
$this->output->write(sprintf("\x1b[%dG", $column));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function moveToPosition(int $column, int $row): self
|
||||
{
|
||||
$this->output->write(sprintf("\x1b[%d;%dH", $row + 1, $column));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function savePosition(): self
|
||||
{
|
||||
$this->output->write("\x1b7");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function restorePosition(): self
|
||||
{
|
||||
$this->output->write("\x1b8");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hide(): self
|
||||
{
|
||||
$this->output->write("\x1b[?25l");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function show(): self
|
||||
{
|
||||
$this->output->write("\x1b[?25h\x1b[?0c");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all the output from the current line.
|
||||
*/
|
||||
public function clearLine(): self
|
||||
{
|
||||
$this->output->write("\x1b[2K");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all the output from the current line after the current position.
|
||||
*/
|
||||
public function clearLineAfter(): self
|
||||
{
|
||||
$this->output->write("\x1b[K");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all the output from the cursors' current position to the end of the screen.
|
||||
*/
|
||||
public function clearOutput(): self
|
||||
{
|
||||
$this->output->write("\x1b[0J");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the entire screen.
|
||||
*/
|
||||
public function clearScreen(): self
|
||||
{
|
||||
$this->output->write("\x1b[2J");
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current cursor position as x,y coordinates.
|
||||
*/
|
||||
public function getCurrentPosition(): array
|
||||
{
|
||||
static $isTtySupported;
|
||||
|
||||
if (null === $isTtySupported && \function_exists('proc_open')) {
|
||||
$isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes);
|
||||
}
|
||||
|
||||
if (!$isTtySupported) {
|
||||
return [1, 1];
|
||||
}
|
||||
|
||||
$sttyMode = shell_exec('stty -g');
|
||||
shell_exec('stty -icanon -echo');
|
||||
|
||||
@fwrite($this->input, "\033[6n");
|
||||
|
||||
$code = trim(fread($this->input, 1024));
|
||||
|
||||
shell_exec(sprintf('stty %s', $sttyMode));
|
||||
|
||||
sscanf($code, "\033[%d;%dR", $row, $col);
|
||||
|
||||
return [$col, $row];
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,14 @@
|
||||
namespace Symfony\Component\Console\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Command\LazyCommand;
|
||||
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\TypedReference;
|
||||
|
||||
/**
|
||||
@@ -28,11 +31,19 @@ class AddConsoleCommandPass implements CompilerPassInterface
|
||||
{
|
||||
private $commandLoaderServiceId;
|
||||
private $commandTag;
|
||||
private $noPreloadTag;
|
||||
private $privateTagName;
|
||||
|
||||
public function __construct(string $commandLoaderServiceId = 'console.command_loader', string $commandTag = 'console.command')
|
||||
public function __construct(string $commandLoaderServiceId = 'console.command_loader', string $commandTag = 'console.command', string $noPreloadTag = 'container.no_preload', string $privateTagName = 'container.private')
|
||||
{
|
||||
if (0 < \func_num_args()) {
|
||||
trigger_deprecation('symfony/console', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
|
||||
}
|
||||
|
||||
$this->commandLoaderServiceId = $commandLoaderServiceId;
|
||||
$this->commandTag = $commandTag;
|
||||
$this->noPreloadTag = $noPreloadTag;
|
||||
$this->privateTagName = $privateTagName;
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container)
|
||||
@@ -44,10 +55,11 @@ class AddConsoleCommandPass implements CompilerPassInterface
|
||||
|
||||
foreach ($commandServices as $id => $tags) {
|
||||
$definition = $container->getDefinition($id);
|
||||
$definition->addTag($this->noPreloadTag);
|
||||
$class = $container->getParameterBag()->resolveValue($definition->getClass());
|
||||
|
||||
if (isset($tags[0]['command'])) {
|
||||
$commandName = $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));
|
||||
@@ -55,11 +67,18 @@ class AddConsoleCommandPass implements CompilerPassInterface
|
||||
if (!$r->isSubclassOf(Command::class)) {
|
||||
throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class));
|
||||
}
|
||||
$commandName = $class::getDefaultName();
|
||||
$aliases = $class::getDefaultName();
|
||||
}
|
||||
|
||||
$aliases = explode('|', $aliases ?? '');
|
||||
$commandName = array_shift($aliases);
|
||||
|
||||
if ($isHidden = '' === $commandName) {
|
||||
$commandName = array_shift($aliases);
|
||||
}
|
||||
|
||||
if (null === $commandName) {
|
||||
if (!$definition->isPublic() || $definition->isPrivate()) {
|
||||
if (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag($this->privateTagName)) {
|
||||
$commandId = 'console.command.public_alias.'.$id;
|
||||
$container->setAlias($commandId, $id)->setPublic(true);
|
||||
$id = $commandId;
|
||||
@@ -69,16 +88,23 @@ class AddConsoleCommandPass implements CompilerPassInterface
|
||||
continue;
|
||||
}
|
||||
|
||||
$description = $tags[0]['description'] ?? null;
|
||||
|
||||
unset($tags[0]);
|
||||
$lazyCommandMap[$commandName] = $id;
|
||||
$lazyCommandRefs[$id] = new TypedReference($id, $class);
|
||||
$aliases = [];
|
||||
|
||||
foreach ($aliases as $alias) {
|
||||
$lazyCommandMap[$alias] = $id;
|
||||
}
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if (isset($tag['command'])) {
|
||||
$aliases[] = $tag['command'];
|
||||
$lazyCommandMap[$tag['command']] = $id;
|
||||
}
|
||||
|
||||
$description = $description ?? $tag['description'] ?? null;
|
||||
}
|
||||
|
||||
$definition->addMethodCall('setName', [$commandName]);
|
||||
@@ -86,11 +112,35 @@ class AddConsoleCommandPass implements CompilerPassInterface
|
||||
if ($aliases) {
|
||||
$definition->addMethodCall('setAliases', [$aliases]);
|
||||
}
|
||||
|
||||
if ($isHidden) {
|
||||
$definition->addMethodCall('setHidden', [true]);
|
||||
}
|
||||
|
||||
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, $this->commandTag, Command::class));
|
||||
}
|
||||
$description = $class::getDefaultDescription();
|
||||
}
|
||||
|
||||
if ($description) {
|
||||
$definition->addMethodCall('setDescription', [$description]);
|
||||
|
||||
$container->register('.'.$id.'.lazy', LazyCommand::class)
|
||||
->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]);
|
||||
|
||||
$lazyCommandRefs[$id] = new Reference('.'.$id.'.lazy');
|
||||
}
|
||||
}
|
||||
|
||||
$container
|
||||
->register($this->commandLoaderServiceId, ContainerCommandLoader::class)
|
||||
->setPublic(true)
|
||||
->addTag($this->noPreloadTag)
|
||||
->setArguments([ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap]);
|
||||
|
||||
$container->setParameter('console.command.ids', $serviceIds);
|
||||
|
||||
@@ -22,7 +22,7 @@ use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
*/
|
||||
class ApplicationDescription
|
||||
{
|
||||
const GLOBAL_NAMESPACE = '_global';
|
||||
public const GLOBAL_NAMESPACE = '_global';
|
||||
|
||||
private $application;
|
||||
private $namespace;
|
||||
@@ -80,7 +80,7 @@ class ApplicationDescription
|
||||
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
|
||||
}
|
||||
|
||||
return isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name];
|
||||
return $this->commands[$name] ?? $this->aliases[$name];
|
||||
}
|
||||
|
||||
private function inspectApplication()
|
||||
|
||||
+2
-12
@@ -34,7 +34,7 @@ abstract class Descriptor implements DescriptorInterface
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function describe(OutputInterface $output, $object, array $options = [])
|
||||
public function describe(OutputInterface $output, object $object, array $options = [])
|
||||
{
|
||||
$this->output = $output;
|
||||
|
||||
@@ -55,7 +55,7 @@ abstract class Descriptor implements DescriptorInterface
|
||||
$this->describeApplication($object, $options);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', \get_class($object)));
|
||||
throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,36 +69,26 @@ abstract class Descriptor implements DescriptorInterface
|
||||
|
||||
/**
|
||||
* Describes an InputArgument instance.
|
||||
*
|
||||
* @return string|mixed
|
||||
*/
|
||||
abstract protected function describeInputArgument(InputArgument $argument, array $options = []);
|
||||
|
||||
/**
|
||||
* Describes an InputOption instance.
|
||||
*
|
||||
* @return string|mixed
|
||||
*/
|
||||
abstract protected function describeInputOption(InputOption $option, array $options = []);
|
||||
|
||||
/**
|
||||
* Describes an InputDefinition instance.
|
||||
*
|
||||
* @return string|mixed
|
||||
*/
|
||||
abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []);
|
||||
|
||||
/**
|
||||
* Describes a Command instance.
|
||||
*
|
||||
* @return string|mixed
|
||||
*/
|
||||
abstract protected function describeCommand(Command $command, array $options = []);
|
||||
|
||||
/**
|
||||
* Describes an Application instance.
|
||||
*
|
||||
* @return string|mixed
|
||||
*/
|
||||
abstract protected function describeApplication(Application $application, array $options = []);
|
||||
}
|
||||
|
||||
@@ -20,10 +20,5 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
*/
|
||||
interface DescriptorInterface
|
||||
{
|
||||
/**
|
||||
* Describes an object if supported.
|
||||
*
|
||||
* @param object $object
|
||||
*/
|
||||
public function describe(OutputInterface $output, $object, array $options = []);
|
||||
public function describe(OutputInterface $output, object $object, array $options = []);
|
||||
}
|
||||
|
||||
+42
-17
@@ -40,6 +40,9 @@ class JsonDescriptor extends Descriptor
|
||||
protected function describeInputOption(InputOption $option, array $options = [])
|
||||
{
|
||||
$this->writeData($this->getInputOptionData($option), $options);
|
||||
if ($option->isNegatable()) {
|
||||
$this->writeData($this->getInputOptionData($option, true), $options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,7 +58,7 @@ class JsonDescriptor extends Descriptor
|
||||
*/
|
||||
protected function describeCommand(Command $command, array $options = [])
|
||||
{
|
||||
$this->writeData($this->getCommandData($command), $options);
|
||||
$this->writeData($this->getCommandData($command, $options['short'] ?? false), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,12 +66,12 @@ class JsonDescriptor extends Descriptor
|
||||
*/
|
||||
protected function describeApplication(Application $application, array $options = [])
|
||||
{
|
||||
$describedNamespace = isset($options['namespace']) ? $options['namespace'] : null;
|
||||
$describedNamespace = $options['namespace'] ?? null;
|
||||
$description = new ApplicationDescription($application, $describedNamespace, true);
|
||||
$commands = [];
|
||||
|
||||
foreach ($description->getCommands() as $command) {
|
||||
$commands[] = $this->getCommandData($command);
|
||||
$commands[] = $this->getCommandData($command, $options['short'] ?? false);
|
||||
}
|
||||
|
||||
$data = [];
|
||||
@@ -95,7 +98,7 @@ class JsonDescriptor extends Descriptor
|
||||
*/
|
||||
private function writeData(array $data, array $options)
|
||||
{
|
||||
$flags = isset($options['json_encoding']) ? $options['json_encoding'] : 0;
|
||||
$flags = $options['json_encoding'] ?? 0;
|
||||
|
||||
$this->write(json_encode($data, $flags));
|
||||
}
|
||||
@@ -107,20 +110,28 @@ class JsonDescriptor extends Descriptor
|
||||
'is_required' => $argument->isRequired(),
|
||||
'is_array' => $argument->isArray(),
|
||||
'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $argument->getDescription()),
|
||||
'default' => INF === $argument->getDefault() ? 'INF' : $argument->getDefault(),
|
||||
'default' => \INF === $argument->getDefault() ? 'INF' : $argument->getDefault(),
|
||||
];
|
||||
}
|
||||
|
||||
private function getInputOptionData(InputOption $option): array
|
||||
private function getInputOptionData(InputOption $option, bool $negated = false): array
|
||||
{
|
||||
return [
|
||||
return $negated ? [
|
||||
'name' => '--no-'.$option->getName(),
|
||||
'shortcut' => '',
|
||||
'accept_value' => false,
|
||||
'is_value_required' => false,
|
||||
'is_multiple' => false,
|
||||
'description' => 'Negate the "--'.$option->getName().'" option',
|
||||
'default' => false,
|
||||
] : [
|
||||
'name' => '--'.$option->getName(),
|
||||
'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '',
|
||||
'accept_value' => $option->acceptValue(),
|
||||
'is_value_required' => $option->isValueRequired(),
|
||||
'is_multiple' => $option->isArray(),
|
||||
'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()),
|
||||
'default' => INF === $option->getDefault() ? 'INF' : $option->getDefault(),
|
||||
'default' => \INF === $option->getDefault() ? 'INF' : $option->getDefault(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -134,23 +145,37 @@ class JsonDescriptor extends Descriptor
|
||||
$inputOptions = [];
|
||||
foreach ($definition->getOptions() as $name => $option) {
|
||||
$inputOptions[$name] = $this->getInputOptionData($option);
|
||||
if ($option->isNegatable()) {
|
||||
$inputOptions['no-'.$name] = $this->getInputOptionData($option, true);
|
||||
}
|
||||
}
|
||||
|
||||
return ['arguments' => $inputArguments, 'options' => $inputOptions];
|
||||
}
|
||||
|
||||
private function getCommandData(Command $command): array
|
||||
private function getCommandData(Command $command, bool $short = false): array
|
||||
{
|
||||
$command->getSynopsis();
|
||||
$command->mergeApplicationDefinition(false);
|
||||
|
||||
return [
|
||||
$data = [
|
||||
'name' => $command->getName(),
|
||||
'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()),
|
||||
'description' => $command->getDescription(),
|
||||
'help' => $command->getProcessedHelp(),
|
||||
'definition' => $this->getInputDefinitionData($command->getNativeDefinition()),
|
||||
'hidden' => $command->isHidden(),
|
||||
];
|
||||
|
||||
if ($short) {
|
||||
$data += [
|
||||
'usage' => $command->getAliases(),
|
||||
];
|
||||
} else {
|
||||
$command->mergeApplicationDefinition(false);
|
||||
|
||||
$data += [
|
||||
'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()),
|
||||
'help' => $command->getProcessedHelp(),
|
||||
'definition' => $this->getInputDefinitionData($command->getDefinition()),
|
||||
];
|
||||
}
|
||||
|
||||
$data['hidden'] = $command->isHidden();
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
+26
-8
@@ -31,7 +31,7 @@ class MarkdownDescriptor extends Descriptor
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function describe(OutputInterface $output, $object, array $options = [])
|
||||
public function describe(OutputInterface $output, object $object, array $options = [])
|
||||
{
|
||||
$decorated = $output->isDecorated();
|
||||
$output->setDecorated(false);
|
||||
@@ -69,6 +69,9 @@ class MarkdownDescriptor extends Descriptor
|
||||
protected function describeInputOption(InputOption $option, array $options = [])
|
||||
{
|
||||
$name = '--'.$option->getName();
|
||||
if ($option->isNegatable()) {
|
||||
$name .= '|--no-'.$option->getName();
|
||||
}
|
||||
if ($option->getShortcut()) {
|
||||
$name .= '|-'.str_replace('|', '|-', $option->getShortcut()).'';
|
||||
}
|
||||
@@ -79,6 +82,7 @@ class MarkdownDescriptor extends Descriptor
|
||||
.'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n"
|
||||
.'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n"
|
||||
.'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n"
|
||||
.'* Is negatable: '.($option->isNegatable() ? 'yes' : 'no')."\n"
|
||||
.'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`'
|
||||
);
|
||||
}
|
||||
@@ -118,12 +122,25 @@ class MarkdownDescriptor extends Descriptor
|
||||
*/
|
||||
protected function describeCommand(Command $command, array $options = [])
|
||||
{
|
||||
$command->getSynopsis();
|
||||
if ($options['short'] ?? false) {
|
||||
$this->write(
|
||||
'`'.$command->getName()."`\n"
|
||||
.str_repeat('-', Helper::width($command->getName()) + 2)."\n\n"
|
||||
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
|
||||
.'### Usage'."\n\n"
|
||||
.array_reduce($command->getAliases(), function ($carry, $usage) {
|
||||
return $carry.'* `'.$usage.'`'."\n";
|
||||
})
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$command->mergeApplicationDefinition(false);
|
||||
|
||||
$this->write(
|
||||
'`'.$command->getName()."`\n"
|
||||
.str_repeat('-', Helper::strlen($command->getName()) + 2)."\n\n"
|
||||
.str_repeat('-', Helper::width($command->getName()) + 2)."\n\n"
|
||||
.($command->getDescription() ? $command->getDescription()."\n\n" : '')
|
||||
.'### Usage'."\n\n"
|
||||
.array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), function ($carry, $usage) {
|
||||
@@ -136,9 +153,10 @@ class MarkdownDescriptor extends Descriptor
|
||||
$this->write($help);
|
||||
}
|
||||
|
||||
if ($command->getNativeDefinition()) {
|
||||
$definition = $command->getDefinition();
|
||||
if ($definition->getOptions() || $definition->getArguments()) {
|
||||
$this->write("\n\n");
|
||||
$this->describeInputDefinition($command->getNativeDefinition());
|
||||
$this->describeInputDefinition($definition);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,11 +165,11 @@ class MarkdownDescriptor extends Descriptor
|
||||
*/
|
||||
protected function describeApplication(Application $application, array $options = [])
|
||||
{
|
||||
$describedNamespace = isset($options['namespace']) ? $options['namespace'] : null;
|
||||
$describedNamespace = $options['namespace'] ?? null;
|
||||
$description = new ApplicationDescription($application, $describedNamespace);
|
||||
$title = $this->getApplicationTitle($application);
|
||||
|
||||
$this->write($title."\n".str_repeat('=', Helper::strlen($title)));
|
||||
$this->write($title."\n".str_repeat('=', Helper::width($title)));
|
||||
|
||||
foreach ($description->getNamespaces() as $namespace) {
|
||||
if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
|
||||
@@ -167,7 +185,7 @@ class MarkdownDescriptor extends Descriptor
|
||||
|
||||
foreach ($description->getCommands() as $command) {
|
||||
$this->write("\n\n");
|
||||
if (null !== $describeCommand = $this->describeCommand($command)) {
|
||||
if (null !== $describeCommand = $this->describeCommand($command, $options)) {
|
||||
$this->write($describeCommand);
|
||||
}
|
||||
}
|
||||
|
||||
+21
-22
@@ -39,7 +39,7 @@ class TextDescriptor extends Descriptor
|
||||
$default = '';
|
||||
}
|
||||
|
||||
$totalWidth = isset($options['total_width']) ? $options['total_width'] : Helper::strlen($argument->getName());
|
||||
$totalWidth = $options['total_width'] ?? Helper::width($argument->getName());
|
||||
$spacingWidth = $totalWidth - \strlen($argument->getName());
|
||||
|
||||
$this->writeText(sprintf(' <info>%s</info> %s%s%s',
|
||||
@@ -71,13 +71,13 @@ class TextDescriptor extends Descriptor
|
||||
}
|
||||
}
|
||||
|
||||
$totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]);
|
||||
$totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]);
|
||||
$synopsis = sprintf('%s%s',
|
||||
$option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ',
|
||||
sprintf('--%s%s', $option->getName(), $value)
|
||||
sprintf($option->isNegatable() ? '--%1$s|--no-%1$s' : '--%1$s%2$s', $option->getName(), $value)
|
||||
);
|
||||
|
||||
$spacingWidth = $totalWidth - Helper::strlen($synopsis);
|
||||
$spacingWidth = $totalWidth - Helper::width($synopsis);
|
||||
|
||||
$this->writeText(sprintf(' <info>%s</info> %s%s%s%s',
|
||||
$synopsis,
|
||||
@@ -96,7 +96,7 @@ class TextDescriptor extends Descriptor
|
||||
{
|
||||
$totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions());
|
||||
foreach ($definition->getArguments() as $argument) {
|
||||
$totalWidth = max($totalWidth, Helper::strlen($argument->getName()));
|
||||
$totalWidth = max($totalWidth, Helper::width($argument->getName()));
|
||||
}
|
||||
|
||||
if ($definition->getArguments()) {
|
||||
@@ -117,7 +117,7 @@ class TextDescriptor extends Descriptor
|
||||
|
||||
$this->writeText('<comment>Options:</comment>', $options);
|
||||
foreach ($definition->getOptions() as $option) {
|
||||
if (\strlen($option->getShortcut()) > 1) {
|
||||
if (\strlen($option->getShortcut() ?? '') > 1) {
|
||||
$laterOptions[] = $option;
|
||||
continue;
|
||||
}
|
||||
@@ -136,8 +136,6 @@ class TextDescriptor extends Descriptor
|
||||
*/
|
||||
protected function describeCommand(Command $command, array $options = [])
|
||||
{
|
||||
$command->getSynopsis(true);
|
||||
$command->getSynopsis(false);
|
||||
$command->mergeApplicationDefinition(false);
|
||||
|
||||
if ($description = $command->getDescription()) {
|
||||
@@ -154,7 +152,7 @@ class TextDescriptor extends Descriptor
|
||||
}
|
||||
$this->writeText("\n");
|
||||
|
||||
$definition = $command->getNativeDefinition();
|
||||
$definition = $command->getDefinition();
|
||||
if ($definition->getOptions() || $definition->getArguments()) {
|
||||
$this->writeText("\n");
|
||||
$this->describeInputDefinition($definition, $options);
|
||||
@@ -176,7 +174,7 @@ class TextDescriptor extends Descriptor
|
||||
*/
|
||||
protected function describeApplication(Application $application, array $options = [])
|
||||
{
|
||||
$describedNamespace = isset($options['namespace']) ? $options['namespace'] : null;
|
||||
$describedNamespace = $options['namespace'] ?? null;
|
||||
$description = new ApplicationDescription($application, $describedNamespace);
|
||||
|
||||
if (isset($options['raw_text']) && $options['raw_text']) {
|
||||
@@ -212,7 +210,7 @@ class TextDescriptor extends Descriptor
|
||||
// calculate max. width based on available commands per namespace
|
||||
$width = $this->getColumnWidth(array_merge(...array_values(array_map(function ($namespace) use ($commands) {
|
||||
return array_intersect($namespace['commands'], array_keys($commands));
|
||||
}, $namespaces))));
|
||||
}, array_values($namespaces)))));
|
||||
|
||||
if ($describedNamespace) {
|
||||
$this->writeText(sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), $options);
|
||||
@@ -236,7 +234,7 @@ class TextDescriptor extends Descriptor
|
||||
|
||||
foreach ($namespace['commands'] as $name) {
|
||||
$this->writeText("\n");
|
||||
$spacingWidth = $width - Helper::strlen($name);
|
||||
$spacingWidth = $width - Helper::width($name);
|
||||
$command = $commands[$name];
|
||||
$commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : '';
|
||||
$this->writeText(sprintf(' <info>%s</info>%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options);
|
||||
@@ -280,7 +278,7 @@ class TextDescriptor extends Descriptor
|
||||
*/
|
||||
private function formatDefaultValue($default): string
|
||||
{
|
||||
if (INF === $default) {
|
||||
if (\INF === $default) {
|
||||
return 'INF';
|
||||
}
|
||||
|
||||
@@ -294,11 +292,11 @@ class TextDescriptor extends Descriptor
|
||||
}
|
||||
}
|
||||
|
||||
return str_replace('\\\\', '\\', json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
||||
return str_replace('\\\\', '\\', json_encode($default, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param (Command|string)[] $commands
|
||||
* @param array<Command|string> $commands
|
||||
*/
|
||||
private function getColumnWidth(array $commands): int
|
||||
{
|
||||
@@ -306,12 +304,12 @@ class TextDescriptor extends Descriptor
|
||||
|
||||
foreach ($commands as $command) {
|
||||
if ($command instanceof Command) {
|
||||
$widths[] = Helper::strlen($command->getName());
|
||||
$widths[] = Helper::width($command->getName());
|
||||
foreach ($command->getAliases() as $alias) {
|
||||
$widths[] = Helper::strlen($alias);
|
||||
$widths[] = Helper::width($alias);
|
||||
}
|
||||
} else {
|
||||
$widths[] = Helper::strlen($command);
|
||||
$widths[] = Helper::width($command);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,10 +324,11 @@ class TextDescriptor extends Descriptor
|
||||
$totalWidth = 0;
|
||||
foreach ($options as $option) {
|
||||
// "-" + shortcut + ", --" + name
|
||||
$nameLength = 1 + max(Helper::strlen($option->getShortcut()), 1) + 4 + Helper::strlen($option->getName());
|
||||
|
||||
if ($option->acceptValue()) {
|
||||
$valueLength = 1 + Helper::strlen($option->getName()); // = + value
|
||||
$nameLength = 1 + max(Helper::width($option->getShortcut()), 1) + 4 + Helper::width($option->getName());
|
||||
if ($option->isNegatable()) {
|
||||
$nameLength += 6 + Helper::width($option->getName()); // |--no- + name
|
||||
} elseif ($option->acceptValue()) {
|
||||
$valueLength = 1 + Helper::width($option->getName()); // = + value
|
||||
$valueLength += $option->isValueOptional() ? 2 : 0; // [ + ]
|
||||
|
||||
$nameLength += $valueLength;
|
||||
|
||||
+33
-17
@@ -44,37 +44,42 @@ class XmlDescriptor extends Descriptor
|
||||
return $dom;
|
||||
}
|
||||
|
||||
public function getCommandDocument(Command $command): \DOMDocument
|
||||
public function getCommandDocument(Command $command, bool $short = false): \DOMDocument
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$dom->appendChild($commandXML = $dom->createElement('command'));
|
||||
|
||||
$command->getSynopsis();
|
||||
$command->mergeApplicationDefinition(false);
|
||||
|
||||
$commandXML->setAttribute('id', $command->getName());
|
||||
$commandXML->setAttribute('name', $command->getName());
|
||||
$commandXML->setAttribute('hidden', $command->isHidden() ? 1 : 0);
|
||||
|
||||
$commandXML->appendChild($usagesXML = $dom->createElement('usages'));
|
||||
|
||||
foreach (array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()) as $usage) {
|
||||
$usagesXML->appendChild($dom->createElement('usage', $usage));
|
||||
}
|
||||
|
||||
$commandXML->appendChild($descriptionXML = $dom->createElement('description'));
|
||||
$descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription())));
|
||||
|
||||
$commandXML->appendChild($helpXML = $dom->createElement('help'));
|
||||
$helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp())));
|
||||
if ($short) {
|
||||
foreach ($command->getAliases() as $usage) {
|
||||
$usagesXML->appendChild($dom->createElement('usage', $usage));
|
||||
}
|
||||
} else {
|
||||
$command->mergeApplicationDefinition(false);
|
||||
|
||||
$definitionXML = $this->getInputDefinitionDocument($command->getNativeDefinition());
|
||||
$this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0));
|
||||
foreach (array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()) as $usage) {
|
||||
$usagesXML->appendChild($dom->createElement('usage', $usage));
|
||||
}
|
||||
|
||||
$commandXML->appendChild($helpXML = $dom->createElement('help'));
|
||||
$helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp())));
|
||||
|
||||
$definitionXML = $this->getInputDefinitionDocument($command->getDefinition());
|
||||
$this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0));
|
||||
}
|
||||
|
||||
return $dom;
|
||||
}
|
||||
|
||||
public function getApplicationDocument(Application $application, string $namespace = null): \DOMDocument
|
||||
public function getApplicationDocument(Application $application, string $namespace = null, bool $short = false): \DOMDocument
|
||||
{
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$dom->appendChild($rootXml = $dom->createElement('symfony'));
|
||||
@@ -95,7 +100,7 @@ class XmlDescriptor extends Descriptor
|
||||
}
|
||||
|
||||
foreach ($description->getCommands() as $command) {
|
||||
$this->appendDocument($commandsXML, $this->getCommandDocument($command));
|
||||
$this->appendDocument($commandsXML, $this->getCommandDocument($command, $short));
|
||||
}
|
||||
|
||||
if (!$namespace) {
|
||||
@@ -144,7 +149,7 @@ class XmlDescriptor extends Descriptor
|
||||
*/
|
||||
protected function describeCommand(Command $command, array $options = [])
|
||||
{
|
||||
$this->writeDocument($this->getCommandDocument($command));
|
||||
$this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,7 +157,7 @@ class XmlDescriptor extends Descriptor
|
||||
*/
|
||||
protected function describeApplication(Application $application, array $options = [])
|
||||
{
|
||||
$this->writeDocument($this->getApplicationDocument($application, isset($options['namespace']) ? $options['namespace'] : null));
|
||||
$this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,7 +206,7 @@ class XmlDescriptor extends Descriptor
|
||||
|
||||
$dom->appendChild($objectXML = $dom->createElement('option'));
|
||||
$objectXML->setAttribute('name', '--'.$option->getName());
|
||||
$pos = strpos($option->getShortcut(), '|');
|
||||
$pos = strpos($option->getShortcut() ?? '', '|');
|
||||
if (false !== $pos) {
|
||||
$objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos));
|
||||
$objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut()));
|
||||
@@ -226,6 +231,17 @@ class XmlDescriptor extends Descriptor
|
||||
}
|
||||
}
|
||||
|
||||
if ($option->isNegatable()) {
|
||||
$dom->appendChild($objectXML = $dom->createElement('option'));
|
||||
$objectXML->setAttribute('name', '--no-'.$option->getName());
|
||||
$objectXML->setAttribute('shortcut', '');
|
||||
$objectXML->setAttribute('accept_value', 0);
|
||||
$objectXML->setAttribute('is_value_required', 0);
|
||||
$objectXML->setAttribute('is_multiple', 0);
|
||||
$objectXML->appendChild($descriptionXML = $dom->createElement('description'));
|
||||
$descriptionXML->appendChild($dom->createTextNode('Negate the "--'.$option->getName().'" option'));
|
||||
}
|
||||
|
||||
return $dom;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ final class ConsoleCommandEvent extends ConsoleEvent
|
||||
/**
|
||||
* The return code for skipped commands, this will also be passed into the terminate event.
|
||||
*/
|
||||
const RETURN_CODE_DISABLED = 113;
|
||||
public const RETURN_CODE_DISABLED = 113;
|
||||
|
||||
/**
|
||||
* Indicates if the command should be run or skipped.
|
||||
|
||||
+1
-1
@@ -53,6 +53,6 @@ final class ConsoleErrorEvent extends ConsoleEvent
|
||||
|
||||
public function getExitCode(): int
|
||||
{
|
||||
return null !== $this->exitCode ? $this->exitCode : (\is_int($this->error->getCode()) && 0 !== $this->error->getCode() ? $this->error->getCode() : 1);
|
||||
return $this->exitCode ?? (\is_int($this->error->getCode()) && 0 !== $this->error->getCode() ? $this->error->getCode() : 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?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\Event;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author marie <marie@users.noreply.github.com>
|
||||
*/
|
||||
final class ConsoleSignalEvent extends ConsoleEvent
|
||||
{
|
||||
private $handlingSignal;
|
||||
|
||||
public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal)
|
||||
{
|
||||
parent::__construct($command, $input, $output);
|
||||
$this->handlingSignal = $handlingSignal;
|
||||
}
|
||||
|
||||
public function getHandlingSignal(): int
|
||||
{
|
||||
return $this->handlingSignal;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -40,12 +40,12 @@ class ErrorListener implements EventSubscriberInterface
|
||||
$error = $event->getError();
|
||||
|
||||
if (!$inputString = $this->getInputString($event)) {
|
||||
$this->logger->error('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]);
|
||||
$this->logger->critical('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->error('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]);
|
||||
$this->logger->critical('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]);
|
||||
}
|
||||
|
||||
public function onConsoleTerminate(ConsoleTerminateEvent $event)
|
||||
|
||||
@@ -21,10 +21,10 @@ class CommandNotFoundException extends \InvalidArgumentException implements Exce
|
||||
private $alternatives;
|
||||
|
||||
/**
|
||||
* @param string $message Exception message to throw
|
||||
* @param array $alternatives List of similar defined names
|
||||
* @param int $code Exception code
|
||||
* @param \Throwable $previous Previous exception used for the exception chaining
|
||||
* @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)
|
||||
{
|
||||
@@ -34,7 +34,7 @@ class CommandNotFoundException extends \InvalidArgumentException implements Exce
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array A list of similar defined names
|
||||
* @return string[] A list of similar defined names
|
||||
*/
|
||||
public function getAlternatives()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?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\Formatter;
|
||||
|
||||
/**
|
||||
* @author Tien Xuan Vo <tien.xuan.vo@gmail.com>
|
||||
*/
|
||||
final class NullOutputFormatter implements OutputFormatterInterface
|
||||
{
|
||||
private $style;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function format(?string $message): void
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStyle(string $name): OutputFormatterStyleInterface
|
||||
{
|
||||
if ($this->style) {
|
||||
return $this->style;
|
||||
}
|
||||
// to comply with the interface we must return a OutputFormatterStyleInterface
|
||||
return $this->style = new NullOutputFormatterStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function hasStyle(string $name): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isDecorated(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setDecorated(bool $decorated): void
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setStyle(string $name, OutputFormatterStyleInterface $style): void
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?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\Formatter;
|
||||
|
||||
/**
|
||||
* @author Tien Xuan Vo <tien.xuan.vo@gmail.com>
|
||||
*/
|
||||
final class NullOutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function apply(string $text): string
|
||||
{
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setBackground(string $color = null): void
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setForeground(string $color = null): void
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setOption(string $option): void
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setOptions(array $options): void
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function unsetOption(string $option): void
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
+13
-5
@@ -25,6 +25,14 @@ class OutputFormatter implements WrappableOutputFormatterInterface
|
||||
private $styles = [];
|
||||
private $styleStack;
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->styleStack = clone $this->styleStack;
|
||||
foreach ($this->styles as $key => $value) {
|
||||
$this->styles[$key] = clone $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes "<" special char in given text.
|
||||
*
|
||||
@@ -44,7 +52,7 @@ class OutputFormatter implements WrappableOutputFormatterInterface
|
||||
*/
|
||||
public static function escapeTrailingBackslash(string $text): string
|
||||
{
|
||||
if ('\\' === substr($text, -1)) {
|
||||
if (str_ends_with($text, '\\')) {
|
||||
$len = \strlen($text);
|
||||
$text = rtrim($text, '\\');
|
||||
$text = str_replace("\0", '', $text);
|
||||
@@ -136,7 +144,7 @@ class OutputFormatter implements WrappableOutputFormatterInterface
|
||||
$output = '';
|
||||
$tagRegex = '[a-z][^<>]*+';
|
||||
$currentLineLength = 0;
|
||||
preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, PREG_OFFSET_CAPTURE);
|
||||
preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE);
|
||||
foreach ($matches[0] as $i => $match) {
|
||||
$pos = $match[1];
|
||||
$text = $match[0];
|
||||
@@ -153,7 +161,7 @@ class OutputFormatter implements WrappableOutputFormatterInterface
|
||||
if ($open = '/' != $text[1]) {
|
||||
$tag = $matches[1][$i][0];
|
||||
} else {
|
||||
$tag = isset($matches[3][$i][0]) ? $matches[3][$i][0] : '';
|
||||
$tag = $matches[3][$i][0] ?? '';
|
||||
}
|
||||
|
||||
if (!$open && !$tag) {
|
||||
@@ -170,7 +178,7 @@ class OutputFormatter implements WrappableOutputFormatterInterface
|
||||
|
||||
$output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength);
|
||||
|
||||
if (false !== strpos($output, "\0")) {
|
||||
if (str_contains($output, "\0")) {
|
||||
return strtr($output, ["\0" => '\\', '\\<' => '<']);
|
||||
}
|
||||
|
||||
@@ -194,7 +202,7 @@ class OutputFormatter implements WrappableOutputFormatterInterface
|
||||
return $this->styles[$string];
|
||||
}
|
||||
|
||||
if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, PREG_SET_ORDER)) {
|
||||
if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, \PREG_SET_ORDER)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+15
-103
@@ -11,7 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Console\Formatter;
|
||||
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Color;
|
||||
|
||||
/**
|
||||
* Formatter style class for defining styles.
|
||||
@@ -20,40 +20,11 @@ use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
*/
|
||||
class OutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
{
|
||||
private static $availableForegroundColors = [
|
||||
'black' => ['set' => 30, 'unset' => 39],
|
||||
'red' => ['set' => 31, 'unset' => 39],
|
||||
'green' => ['set' => 32, 'unset' => 39],
|
||||
'yellow' => ['set' => 33, 'unset' => 39],
|
||||
'blue' => ['set' => 34, 'unset' => 39],
|
||||
'magenta' => ['set' => 35, 'unset' => 39],
|
||||
'cyan' => ['set' => 36, 'unset' => 39],
|
||||
'white' => ['set' => 37, 'unset' => 39],
|
||||
'default' => ['set' => 39, 'unset' => 39],
|
||||
];
|
||||
private static $availableBackgroundColors = [
|
||||
'black' => ['set' => 40, 'unset' => 49],
|
||||
'red' => ['set' => 41, 'unset' => 49],
|
||||
'green' => ['set' => 42, 'unset' => 49],
|
||||
'yellow' => ['set' => 43, 'unset' => 49],
|
||||
'blue' => ['set' => 44, 'unset' => 49],
|
||||
'magenta' => ['set' => 45, 'unset' => 49],
|
||||
'cyan' => ['set' => 46, 'unset' => 49],
|
||||
'white' => ['set' => 47, 'unset' => 49],
|
||||
'default' => ['set' => 49, 'unset' => 49],
|
||||
];
|
||||
private static $availableOptions = [
|
||||
'bold' => ['set' => 1, 'unset' => 22],
|
||||
'underscore' => ['set' => 4, 'unset' => 24],
|
||||
'blink' => ['set' => 5, 'unset' => 25],
|
||||
'reverse' => ['set' => 7, 'unset' => 27],
|
||||
'conceal' => ['set' => 8, 'unset' => 28],
|
||||
];
|
||||
|
||||
private $color;
|
||||
private $foreground;
|
||||
private $background;
|
||||
private $options;
|
||||
private $href;
|
||||
private $options = [];
|
||||
private $handlesHrefGracefully;
|
||||
|
||||
/**
|
||||
@@ -64,15 +35,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
*/
|
||||
public function __construct(string $foreground = null, string $background = null, array $options = [])
|
||||
{
|
||||
if (null !== $foreground) {
|
||||
$this->setForeground($foreground);
|
||||
}
|
||||
if (null !== $background) {
|
||||
$this->setBackground($background);
|
||||
}
|
||||
if (\count($options)) {
|
||||
$this->setOptions($options);
|
||||
}
|
||||
$this->color = new Color($this->foreground = $foreground ?: '', $this->background = $background ?: '', $this->options = $options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,17 +43,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
*/
|
||||
public function setForeground(string $color = null)
|
||||
{
|
||||
if (null === $color) {
|
||||
$this->foreground = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset(static::$availableForegroundColors[$color])) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s).', $color, implode(', ', array_keys(static::$availableForegroundColors))));
|
||||
}
|
||||
|
||||
$this->foreground = static::$availableForegroundColors[$color];
|
||||
$this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,17 +51,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
*/
|
||||
public function setBackground(string $color = null)
|
||||
{
|
||||
if (null === $color) {
|
||||
$this->background = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset(static::$availableBackgroundColors[$color])) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s).', $color, implode(', ', array_keys(static::$availableBackgroundColors))));
|
||||
}
|
||||
|
||||
$this->background = static::$availableBackgroundColors[$color];
|
||||
$this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options);
|
||||
}
|
||||
|
||||
public function setHref(string $url): void
|
||||
@@ -121,13 +64,8 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
*/
|
||||
public function setOption(string $option)
|
||||
{
|
||||
if (!isset(static::$availableOptions[$option])) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(static::$availableOptions))));
|
||||
}
|
||||
|
||||
if (!\in_array(static::$availableOptions[$option], $this->options)) {
|
||||
$this->options[] = static::$availableOptions[$option];
|
||||
}
|
||||
$this->options[] = $option;
|
||||
$this->color = new Color($this->foreground, $this->background, $this->options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,14 +73,12 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
*/
|
||||
public function unsetOption(string $option)
|
||||
{
|
||||
if (!isset(static::$availableOptions[$option])) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(static::$availableOptions))));
|
||||
}
|
||||
|
||||
$pos = array_search(static::$availableOptions[$option], $this->options);
|
||||
$pos = array_search($option, $this->options);
|
||||
if (false !== $pos) {
|
||||
unset($this->options[$pos]);
|
||||
}
|
||||
|
||||
$this->color = new Color($this->foreground, $this->background, $this->options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,11 +86,7 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
*/
|
||||
public function setOptions(array $options)
|
||||
{
|
||||
$this->options = [];
|
||||
|
||||
foreach ($options as $option) {
|
||||
$this->setOption($option);
|
||||
}
|
||||
$this->color = new Color($this->foreground, $this->background, $this->options = $options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,35 +94,15 @@ class OutputFormatterStyle implements OutputFormatterStyleInterface
|
||||
*/
|
||||
public function apply(string $text)
|
||||
{
|
||||
$setCodes = [];
|
||||
$unsetCodes = [];
|
||||
|
||||
if (null === $this->handlesHrefGracefully) {
|
||||
$this->handlesHrefGracefully = 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR') && !getenv('KONSOLE_VERSION');
|
||||
}
|
||||
|
||||
if (null !== $this->foreground) {
|
||||
$setCodes[] = $this->foreground['set'];
|
||||
$unsetCodes[] = $this->foreground['unset'];
|
||||
}
|
||||
if (null !== $this->background) {
|
||||
$setCodes[] = $this->background['set'];
|
||||
$unsetCodes[] = $this->background['unset'];
|
||||
}
|
||||
|
||||
foreach ($this->options as $option) {
|
||||
$setCodes[] = $option['set'];
|
||||
$unsetCodes[] = $option['unset'];
|
||||
$this->handlesHrefGracefully = 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR')
|
||||
&& (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100);
|
||||
}
|
||||
|
||||
if (null !== $this->href && $this->handlesHrefGracefully) {
|
||||
$text = "\033]8;;$this->href\033\\$text\033]8;;\033\\";
|
||||
}
|
||||
|
||||
if (0 === \count($setCodes)) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes));
|
||||
return $this->color->apply($text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class OutputFormatterStyleStack implements ResetInterface
|
||||
|
||||
public function __construct(OutputFormatterStyleInterface $emptyStyle = null)
|
||||
{
|
||||
$this->emptyStyle = $emptyStyle ?: new OutputFormatterStyle();
|
||||
$this->emptyStyle = $emptyStyle ?? new OutputFormatterStyle();
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -48,12 +48,12 @@ class FormatterHelper extends Helper
|
||||
foreach ($messages as $message) {
|
||||
$message = OutputFormatter::escape($message);
|
||||
$lines[] = sprintf($large ? ' %s ' : ' %s ', $message);
|
||||
$len = max(self::strlen($message) + ($large ? 4 : 2), $len);
|
||||
$len = max(self::width($message) + ($large ? 4 : 2), $len);
|
||||
}
|
||||
|
||||
$messages = $large ? [str_repeat(' ', $len)] : [];
|
||||
for ($i = 0; isset($lines[$i]); ++$i) {
|
||||
$messages[] = $lines[$i].str_repeat(' ', $len - self::strlen($lines[$i]));
|
||||
$messages[] = $lines[$i].str_repeat(' ', $len - self::width($lines[$i]));
|
||||
}
|
||||
if ($large) {
|
||||
$messages[] = str_repeat(' ', $len);
|
||||
@@ -73,9 +73,9 @@ class FormatterHelper extends Helper
|
||||
*/
|
||||
public function truncate(string $message, int $length, string $suffix = '...')
|
||||
{
|
||||
$computedLength = $length - self::strlen($suffix);
|
||||
$computedLength = $length - self::width($suffix);
|
||||
|
||||
if ($computedLength > self::strlen($message)) {
|
||||
if ($computedLength > self::width($message)) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
|
||||
+51
-5
@@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\Console\Helper;
|
||||
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
|
||||
/**
|
||||
* Helper is the base class for all helper classes.
|
||||
@@ -41,10 +42,29 @@ abstract class Helper implements HelperInterface
|
||||
/**
|
||||
* Returns the length of a string, using mb_strwidth if it is available.
|
||||
*
|
||||
* @deprecated since 5.3
|
||||
*
|
||||
* @return int The length of the string
|
||||
*/
|
||||
public static function strlen(?string $string)
|
||||
{
|
||||
trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::width() or Helper::length() instead.', __METHOD__);
|
||||
|
||||
return self::width($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the width of a string, using mb_strwidth if it is available.
|
||||
* The width is how many characters positions the string will use.
|
||||
*/
|
||||
public static function width(?string $string): int
|
||||
{
|
||||
$string ?? $string = '';
|
||||
|
||||
if (preg_match('//u', $string)) {
|
||||
return (new UnicodeString($string))->width(false);
|
||||
}
|
||||
|
||||
if (false === $encoding = mb_detect_encoding($string, null, true)) {
|
||||
return \strlen($string);
|
||||
}
|
||||
@@ -52,13 +72,34 @@ abstract class Helper implements HelperInterface
|
||||
return mb_strwidth($string, $encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of a string, using mb_strlen if it is available.
|
||||
* The length is related to how many bytes the string will use.
|
||||
*/
|
||||
public static function length(?string $string): int
|
||||
{
|
||||
$string ?? $string = '';
|
||||
|
||||
if (preg_match('//u', $string)) {
|
||||
return (new UnicodeString($string))->length();
|
||||
}
|
||||
|
||||
if (false === $encoding = mb_detect_encoding($string, null, true)) {
|
||||
return \strlen($string);
|
||||
}
|
||||
|
||||
return mb_strlen($string, $encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the subset of a string, using mb_substr if it is available.
|
||||
*
|
||||
* @return string The string subset
|
||||
*/
|
||||
public static function substr(string $string, int $from, int $length = null)
|
||||
public static function substr(?string $string, int $from, int $length = null)
|
||||
{
|
||||
$string ?? $string = '';
|
||||
|
||||
if (false === $encoding = mb_detect_encoding($string, null, true)) {
|
||||
return substr($string, $from, $length);
|
||||
}
|
||||
@@ -112,17 +153,22 @@ abstract class Helper implements HelperInterface
|
||||
return sprintf('%d B', $memory);
|
||||
}
|
||||
|
||||
public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, $string)
|
||||
/**
|
||||
* @deprecated since 5.3
|
||||
*/
|
||||
public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, ?string $string)
|
||||
{
|
||||
return self::strlen(self::removeDecoration($formatter, $string));
|
||||
trigger_deprecation('symfony/console', '5.3', 'Method "%s()" is deprecated and will be removed in Symfony 6.0. Use Helper::removeDecoration() instead.', __METHOD__);
|
||||
|
||||
return self::width(self::removeDecoration($formatter, $string));
|
||||
}
|
||||
|
||||
public static function removeDecoration(OutputFormatterInterface $formatter, $string)
|
||||
public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string)
|
||||
{
|
||||
$isDecorated = $formatter->isDecorated();
|
||||
$formatter->setDecorated(false);
|
||||
// remove <...> formatting
|
||||
$string = $formatter->format($string);
|
||||
$string = $formatter->format($string ?? '');
|
||||
// remove already formatted characters
|
||||
$string = preg_replace("/\033\[[^m]*m/", '', $string);
|
||||
$formatter->setDecorated($isDecorated);
|
||||
|
||||
+5
-1
@@ -36,6 +36,10 @@ class ProcessHelper extends Helper
|
||||
*/
|
||||
public function run(OutputInterface $output, $cmd, string $error = null, callable $callback = null, int $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE): Process
|
||||
{
|
||||
if (!class_exists(Process::class)) {
|
||||
throw new \LogicException('The ProcessHelper cannot be run as the Process component is not installed. Try running "compose require symfony/process".');
|
||||
}
|
||||
|
||||
if ($output instanceof ConsoleOutputInterface) {
|
||||
$output = $output->getErrorOutput();
|
||||
}
|
||||
@@ -47,7 +51,7 @@ class ProcessHelper extends Helper
|
||||
}
|
||||
|
||||
if (!\is_array($cmd)) {
|
||||
throw new \TypeError(sprintf('The "command" argument of "%s()" must be an array or a "%s" instance, "%s" given.', __METHOD__, Process::class, \is_object($cmd) ? \get_class($cmd) : \gettype($cmd)));
|
||||
throw new \TypeError(sprintf('The "command" argument of "%s()" must be an array or a "%s" instance, "%s" given.', __METHOD__, Process::class, get_debug_type($cmd)));
|
||||
}
|
||||
|
||||
if (\is_string($cmd[0] ?? null)) {
|
||||
|
||||
+68
-43
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Console\Helper;
|
||||
|
||||
use Symfony\Component\Console\Cursor;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleSectionOutput;
|
||||
@@ -25,6 +26,16 @@ use Symfony\Component\Console\Terminal;
|
||||
*/
|
||||
final class ProgressBar
|
||||
{
|
||||
public const FORMAT_VERBOSE = 'verbose';
|
||||
public const FORMAT_VERY_VERBOSE = 'very_verbose';
|
||||
public const FORMAT_DEBUG = 'debug';
|
||||
public const FORMAT_NORMAL = 'normal';
|
||||
|
||||
private const FORMAT_VERBOSE_NOMAX = 'verbose_nomax';
|
||||
private const FORMAT_VERY_VERBOSE_NOMAX = 'very_verbose_nomax';
|
||||
private const FORMAT_DEBUG_NOMAX = 'debug_nomax';
|
||||
private const FORMAT_NORMAL_NOMAX = 'normal_nomax';
|
||||
|
||||
private $barWidth = 28;
|
||||
private $barChar;
|
||||
private $emptyBarChar = '-';
|
||||
@@ -47,6 +58,7 @@ final class ProgressBar
|
||||
private $overwrite = true;
|
||||
private $terminal;
|
||||
private $previousMessage;
|
||||
private $cursor;
|
||||
|
||||
private static $formatters;
|
||||
private static $formats;
|
||||
@@ -54,7 +66,7 @@ final class ProgressBar
|
||||
/**
|
||||
* @param int $max Maximum steps (0 if unknown)
|
||||
*/
|
||||
public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 0.1)
|
||||
public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 1 / 25)
|
||||
{
|
||||
if ($output instanceof ConsoleOutputInterface) {
|
||||
$output = $output->getErrorOutput();
|
||||
@@ -78,6 +90,7 @@ final class ProgressBar
|
||||
}
|
||||
|
||||
$this->startTime = time();
|
||||
$this->cursor = new Cursor($output);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,7 +123,7 @@ final class ProgressBar
|
||||
self::$formatters = self::initPlaceholderFormatters();
|
||||
}
|
||||
|
||||
return isset(self::$formatters[$name]) ? self::$formatters[$name] : null;
|
||||
return self::$formatters[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +156,7 @@ final class ProgressBar
|
||||
self::$formats = self::initFormats();
|
||||
}
|
||||
|
||||
return isset(self::$formats[$name]) ? self::$formats[$name] : null;
|
||||
return self::$formats[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,9 +204,27 @@ final class ProgressBar
|
||||
return $this->percent;
|
||||
}
|
||||
|
||||
public function getBarOffset(): int
|
||||
public function getBarOffset(): float
|
||||
{
|
||||
return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? min(5, $this->barWidth / 15) * $this->writeCount : $this->step) % $this->barWidth);
|
||||
return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth);
|
||||
}
|
||||
|
||||
public function getEstimated(): float
|
||||
{
|
||||
if (!$this->step) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return round((time() - $this->startTime) / $this->step * $this->max);
|
||||
}
|
||||
|
||||
public function getRemaining(): float
|
||||
{
|
||||
if (!$this->step) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return round((time() - $this->startTime) / $this->step * ($this->max - $this->step));
|
||||
}
|
||||
|
||||
public function setBarWidth(int $size)
|
||||
@@ -249,7 +280,7 @@ final class ProgressBar
|
||||
/**
|
||||
* Sets the redraw frequency.
|
||||
*
|
||||
* @param int|float $freq The frequency in steps
|
||||
* @param int|null $freq The frequency in steps
|
||||
*/
|
||||
public function setRedrawFrequency(?int $freq)
|
||||
{
|
||||
@@ -357,7 +388,7 @@ final class ProgressBar
|
||||
{
|
||||
$this->format = null;
|
||||
$this->max = max(0, $max);
|
||||
$this->stepWidth = $this->max ? Helper::strlen((string) $this->max) : 4;
|
||||
$this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -441,20 +472,26 @@ final class ProgressBar
|
||||
if ($this->overwrite) {
|
||||
if (null !== $this->previousMessage) {
|
||||
if ($this->output instanceof ConsoleSectionOutput) {
|
||||
$lines = floor(Helper::strlen($message) / $this->terminal->getWidth()) + $this->formatLineCount + 1;
|
||||
$this->output->clear($lines);
|
||||
$messageLines = explode("\n", $message);
|
||||
$lineCount = \count($messageLines);
|
||||
foreach ($messageLines as $messageLine) {
|
||||
$messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine));
|
||||
if ($messageLineLength > $this->terminal->getWidth()) {
|
||||
$lineCount += floor($messageLineLength / $this->terminal->getWidth());
|
||||
}
|
||||
}
|
||||
$this->output->clear($lineCount);
|
||||
} else {
|
||||
// Erase previous lines
|
||||
if ($this->formatLineCount > 0) {
|
||||
$message = str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount).$message;
|
||||
$this->cursor->moveUp($this->formatLineCount);
|
||||
}
|
||||
|
||||
// Move the cursor to the beginning of the line and erase the line
|
||||
$message = "\x0D\x1B[2K$message";
|
||||
$this->cursor->moveToColumn(1);
|
||||
$this->cursor->clearLine();
|
||||
}
|
||||
}
|
||||
} elseif ($this->step > 0) {
|
||||
$message = PHP_EOL.$message;
|
||||
$message = \PHP_EOL.$message;
|
||||
}
|
||||
|
||||
$this->previousMessage = $originalMessage;
|
||||
@@ -469,13 +506,13 @@ final class ProgressBar
|
||||
switch ($this->output->getVerbosity()) {
|
||||
// OutputInterface::VERBOSITY_QUIET: display is disabled anyway
|
||||
case OutputInterface::VERBOSITY_VERBOSE:
|
||||
return $this->max ? 'verbose' : 'verbose_nomax';
|
||||
return $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX;
|
||||
case OutputInterface::VERBOSITY_VERY_VERBOSE:
|
||||
return $this->max ? 'very_verbose' : 'very_verbose_nomax';
|
||||
return $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX;
|
||||
case OutputInterface::VERBOSITY_DEBUG:
|
||||
return $this->max ? 'debug' : 'debug_nomax';
|
||||
return $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX;
|
||||
default:
|
||||
return $this->max ? 'normal' : 'normal_nomax';
|
||||
return $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +523,7 @@ final class ProgressBar
|
||||
$completeBars = $bar->getBarOffset();
|
||||
$display = str_repeat($bar->getBarCharacter(), $completeBars);
|
||||
if ($completeBars < $bar->getBarWidth()) {
|
||||
$emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter());
|
||||
$emptyBars = $bar->getBarWidth() - $completeBars - Helper::length(Helper::removeDecoration($output->getFormatter(), $bar->getProgressCharacter()));
|
||||
$display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars);
|
||||
}
|
||||
|
||||
@@ -500,32 +537,20 @@ final class ProgressBar
|
||||
throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.');
|
||||
}
|
||||
|
||||
if (!$bar->getProgress()) {
|
||||
$remaining = 0;
|
||||
} else {
|
||||
$remaining = round((time() - $bar->getStartTime()) / $bar->getProgress() * ($bar->getMaxSteps() - $bar->getProgress()));
|
||||
}
|
||||
|
||||
return Helper::formatTime($remaining);
|
||||
return Helper::formatTime($bar->getRemaining());
|
||||
},
|
||||
'estimated' => function (self $bar) {
|
||||
if (!$bar->getMaxSteps()) {
|
||||
throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.');
|
||||
}
|
||||
|
||||
if (!$bar->getProgress()) {
|
||||
$estimated = 0;
|
||||
} else {
|
||||
$estimated = round((time() - $bar->getStartTime()) / $bar->getProgress() * $bar->getMaxSteps());
|
||||
}
|
||||
|
||||
return Helper::formatTime($estimated);
|
||||
return Helper::formatTime($bar->getEstimated());
|
||||
},
|
||||
'memory' => function (self $bar) {
|
||||
return Helper::formatMemory(memory_get_usage(true));
|
||||
},
|
||||
'current' => function (self $bar) {
|
||||
return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', STR_PAD_LEFT);
|
||||
return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT);
|
||||
},
|
||||
'max' => function (self $bar) {
|
||||
return $bar->getMaxSteps();
|
||||
@@ -539,17 +564,17 @@ final class ProgressBar
|
||||
private static function initFormats(): array
|
||||
{
|
||||
return [
|
||||
'normal' => ' %current%/%max% [%bar%] %percent:3s%%',
|
||||
'normal_nomax' => ' %current% [%bar%]',
|
||||
self::FORMAT_NORMAL => ' %current%/%max% [%bar%] %percent:3s%%',
|
||||
self::FORMAT_NORMAL_NOMAX => ' %current% [%bar%]',
|
||||
|
||||
'verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%',
|
||||
'verbose_nomax' => ' %current% [%bar%] %elapsed:6s%',
|
||||
self::FORMAT_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%',
|
||||
self::FORMAT_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%',
|
||||
|
||||
'very_verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%',
|
||||
'very_verbose_nomax' => ' %current% [%bar%] %elapsed:6s%',
|
||||
self::FORMAT_VERY_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%',
|
||||
self::FORMAT_VERY_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%',
|
||||
|
||||
'debug' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%',
|
||||
'debug_nomax' => ' %current% [%bar%] %elapsed:6s% %memory:6s%',
|
||||
self::FORMAT_DEBUG => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%',
|
||||
self::FORMAT_DEBUG_NOMAX => ' %current% [%bar%] %elapsed:6s% %memory:6s%',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -575,7 +600,7 @@ final class ProgressBar
|
||||
|
||||
// gets string length for each sub line with multiline format
|
||||
$linesLength = array_map(function ($subLine) {
|
||||
return Helper::strlenWithoutDecoration($this->output->getFormatter(), rtrim($subLine, "\r"));
|
||||
return Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r")));
|
||||
}, explode("\n", $line));
|
||||
|
||||
$linesWidth = max($linesLength);
|
||||
|
||||
+3
-3
@@ -142,7 +142,7 @@ class ProgressIndicator
|
||||
self::$formats = self::initFormats();
|
||||
}
|
||||
|
||||
return isset(self::$formats[$name]) ? self::$formats[$name] : null;
|
||||
return self::$formats[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,7 +170,7 @@ class ProgressIndicator
|
||||
self::$formatters = self::initPlaceholderFormatters();
|
||||
}
|
||||
|
||||
return isset(self::$formatters[$name]) ? self::$formatters[$name] : null;
|
||||
return self::$formatters[$name] ?? null;
|
||||
}
|
||||
|
||||
private function display()
|
||||
@@ -185,7 +185,7 @@ class ProgressIndicator
|
||||
}
|
||||
|
||||
return $matches[0];
|
||||
}, $this->format));
|
||||
}, $this->format ?? ''));
|
||||
}
|
||||
|
||||
private function determineBestFormat(): string
|
||||
|
||||
+164
-63
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Console\Helper;
|
||||
|
||||
use Symfony\Component\Console\Cursor;
|
||||
use Symfony\Component\Console\Exception\MissingInputException;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
@@ -23,6 +24,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Terminal;
|
||||
use function Symfony\Component\String\s;
|
||||
|
||||
/**
|
||||
* The QuestionHelper class provides helpers to interact with the user.
|
||||
@@ -33,7 +35,8 @@ class QuestionHelper extends Helper
|
||||
{
|
||||
private $inputStream;
|
||||
private static $shell;
|
||||
private static $stty;
|
||||
private static $stty = true;
|
||||
private static $stdinIsInteractive;
|
||||
|
||||
/**
|
||||
* Asks a question to the user.
|
||||
@@ -96,7 +99,7 @@ class QuestionHelper extends Helper
|
||||
/**
|
||||
* Asks the question to the user.
|
||||
*
|
||||
* @return bool|mixed|string|null
|
||||
* @return mixed
|
||||
*
|
||||
* @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
|
||||
*/
|
||||
@@ -104,10 +107,10 @@ class QuestionHelper extends Helper
|
||||
{
|
||||
$this->writePrompt($output, $question);
|
||||
|
||||
$inputStream = $this->inputStream ?: STDIN;
|
||||
$inputStream = $this->inputStream ?: \STDIN;
|
||||
$autocomplete = $question->getAutocompleterCallback();
|
||||
|
||||
if (null === $autocomplete || !Terminal::hasSttyAvailable()) {
|
||||
if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) {
|
||||
$ret = false;
|
||||
if ($question->isHidden()) {
|
||||
try {
|
||||
@@ -121,7 +124,7 @@ class QuestionHelper extends Helper
|
||||
}
|
||||
|
||||
if (false === $ret) {
|
||||
$ret = fgets($inputStream, 4096);
|
||||
$ret = $this->readInput($inputStream, $question);
|
||||
if (false === $ret) {
|
||||
throw new MissingInputException('Aborted.');
|
||||
}
|
||||
@@ -164,13 +167,13 @@ class QuestionHelper extends Helper
|
||||
$choices = $question->getChoices();
|
||||
|
||||
if (!$question->isMultiselect()) {
|
||||
return isset($choices[$default]) ? $choices[$default] : $default;
|
||||
return $choices[$default] ?? $default;
|
||||
}
|
||||
|
||||
$default = explode(',', $default);
|
||||
foreach ($default as $k => $v) {
|
||||
$v = $question->isTrimmable() ? trim($v) : $v;
|
||||
$default[$k] = isset($choices[$v]) ? $choices[$v] : $v;
|
||||
$default[$k] = $choices[$v] ?? $v;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,10 +205,10 @@ class QuestionHelper extends Helper
|
||||
{
|
||||
$messages = [];
|
||||
|
||||
$maxWidth = max(array_map('self::strlen', array_keys($choices = $question->getChoices())));
|
||||
$maxWidth = max(array_map('self::width', array_keys($choices = $question->getChoices())));
|
||||
|
||||
foreach ($choices as $key => $value) {
|
||||
$padding = str_repeat(' ', $maxWidth - self::strlen($key));
|
||||
$padding = str_repeat(' ', $maxWidth - self::width($key));
|
||||
|
||||
$messages[] = sprintf(" [<$tag>%s$padding</$tag>] %s", $key, $value);
|
||||
}
|
||||
@@ -234,6 +237,8 @@ class QuestionHelper extends Helper
|
||||
*/
|
||||
private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string
|
||||
{
|
||||
$cursor = new Cursor($output, $inputStream);
|
||||
|
||||
$fullChoice = '';
|
||||
$ret = '';
|
||||
|
||||
@@ -261,9 +266,9 @@ class QuestionHelper extends Helper
|
||||
} elseif ("\177" === $c) { // Backspace Character
|
||||
if (0 === $numMatches && 0 !== $i) {
|
||||
--$i;
|
||||
$cursor->moveLeft(s($fullChoice)->slice(-1)->width(false));
|
||||
|
||||
$fullChoice = self::substr($fullChoice, 0, $i);
|
||||
// Move cursor backwards
|
||||
$output->write("\033[1D");
|
||||
}
|
||||
|
||||
if (0 === $i) {
|
||||
@@ -301,12 +306,12 @@ class QuestionHelper extends Helper
|
||||
$remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))));
|
||||
$output->write($remainingCharacters);
|
||||
$fullChoice .= $remainingCharacters;
|
||||
$i = self::strlen($fullChoice);
|
||||
$i = (false === $encoding = mb_detect_encoding($fullChoice, null, true)) ? \strlen($fullChoice) : mb_strlen($fullChoice, $encoding);
|
||||
|
||||
$matches = array_filter(
|
||||
$autocomplete($ret),
|
||||
function ($match) use ($ret) {
|
||||
return '' === $ret || 0 === strpos($match, $ret);
|
||||
return '' === $ret || str_starts_with($match, $ret);
|
||||
}
|
||||
);
|
||||
$numMatches = \count($matches);
|
||||
@@ -343,23 +348,20 @@ class QuestionHelper extends Helper
|
||||
|
||||
foreach ($autocomplete($ret) as $value) {
|
||||
// If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
|
||||
if (0 === strpos($value, $tempRet)) {
|
||||
if (str_starts_with($value, $tempRet)) {
|
||||
$matches[$numMatches++] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Erase characters from cursor to end of line
|
||||
$output->write("\033[K");
|
||||
$cursor->clearLineAfter();
|
||||
|
||||
if ($numMatches > 0 && -1 !== $ofs) {
|
||||
// Save cursor position
|
||||
$output->write("\0337");
|
||||
$cursor->savePosition();
|
||||
// Write highlighted text, complete the partially entered response
|
||||
$charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)));
|
||||
$output->write('<hl>'.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).'</hl>');
|
||||
// Restore cursor position
|
||||
$output->write("\0338");
|
||||
$cursor->restorePosition();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,12 +374,12 @@ class QuestionHelper extends Helper
|
||||
private function mostRecentlyEnteredValue(string $entered): string
|
||||
{
|
||||
// Determine the most recent value that the user entered
|
||||
if (false === strpos($entered, ',')) {
|
||||
if (!str_contains($entered, ',')) {
|
||||
return $entered;
|
||||
}
|
||||
|
||||
$choices = explode(',', $entered);
|
||||
if (\strlen($lastChoice = trim($choices[\count($choices) - 1])) > 0) {
|
||||
if ('' !== $lastChoice = trim($choices[\count($choices) - 1])) {
|
||||
return $lastChoice;
|
||||
}
|
||||
|
||||
@@ -404,7 +406,7 @@ class QuestionHelper extends Helper
|
||||
$exe = $tmpExe;
|
||||
}
|
||||
|
||||
$sExec = shell_exec($exe);
|
||||
$sExec = shell_exec('"'.$exe.'"');
|
||||
$value = $trimmable ? rtrim($sExec) : $sExec;
|
||||
$output->writeln('');
|
||||
|
||||
@@ -415,35 +417,28 @@ class QuestionHelper extends Helper
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (Terminal::hasSttyAvailable()) {
|
||||
if (self::$stty && Terminal::hasSttyAvailable()) {
|
||||
$sttyMode = shell_exec('stty -g');
|
||||
|
||||
shell_exec('stty -echo');
|
||||
$value = fgets($inputStream, 4096);
|
||||
} elseif ($this->isInteractiveInput($inputStream)) {
|
||||
throw new RuntimeException('Unable to hide the response.');
|
||||
}
|
||||
|
||||
$value = fgets($inputStream, 4096);
|
||||
|
||||
if (self::$stty && Terminal::hasSttyAvailable()) {
|
||||
shell_exec(sprintf('stty %s', $sttyMode));
|
||||
|
||||
if (false === $value) {
|
||||
throw new MissingInputException('Aborted.');
|
||||
}
|
||||
if ($trimmable) {
|
||||
$value = trim($value);
|
||||
}
|
||||
$output->writeln('');
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (false !== $shell = $this->getShell()) {
|
||||
$readCmd = 'csh' === $shell ? 'set mypassword = $<' : 'read -r mypassword';
|
||||
$command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
|
||||
$sCommand = shell_exec($command);
|
||||
$value = $trimmable ? rtrim($sCommand) : $sCommand;
|
||||
$output->writeln('');
|
||||
|
||||
return $value;
|
||||
if (false === $value) {
|
||||
throw new MissingInputException('Aborted.');
|
||||
}
|
||||
if ($trimmable) {
|
||||
$value = trim($value);
|
||||
}
|
||||
$output->writeln('');
|
||||
|
||||
throw new RuntimeException('Unable to hide the response.');
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -459,6 +454,7 @@ class QuestionHelper extends Helper
|
||||
{
|
||||
$error = null;
|
||||
$attempts = $question->getMaxAttempts();
|
||||
|
||||
while (null === $attempts || $attempts--) {
|
||||
if (null !== $error) {
|
||||
$this->writeError($output, $error);
|
||||
@@ -475,30 +471,135 @@ class QuestionHelper extends Helper
|
||||
throw $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid unix shell.
|
||||
*
|
||||
* @return string|bool The valid shell name, false in case no valid shell is found
|
||||
*/
|
||||
private function getShell()
|
||||
private function isInteractiveInput($inputStream): bool
|
||||
{
|
||||
if (null !== self::$shell) {
|
||||
return self::$shell;
|
||||
if ('php://stdin' !== (stream_get_meta_data($inputStream)['uri'] ?? null)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self::$shell = false;
|
||||
if (null !== self::$stdinIsInteractive) {
|
||||
return self::$stdinIsInteractive;
|
||||
}
|
||||
|
||||
if (file_exists('/usr/bin/env')) {
|
||||
// handle other OSs with bash/zsh/ksh/csh if available to hide the answer
|
||||
$test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null";
|
||||
foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) {
|
||||
if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) {
|
||||
self::$shell = $sh;
|
||||
break;
|
||||
}
|
||||
if (\function_exists('stream_isatty')) {
|
||||
return self::$stdinIsInteractive = stream_isatty(fopen('php://stdin', 'r'));
|
||||
}
|
||||
|
||||
if (\function_exists('posix_isatty')) {
|
||||
return self::$stdinIsInteractive = posix_isatty(fopen('php://stdin', 'r'));
|
||||
}
|
||||
|
||||
if (!\function_exists('exec')) {
|
||||
return self::$stdinIsInteractive = true;
|
||||
}
|
||||
|
||||
exec('stty 2> /dev/null', $output, $status);
|
||||
|
||||
return self::$stdinIsInteractive = 1 !== $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads one or more lines of input and returns what is read.
|
||||
*
|
||||
* @param resource $inputStream The handler resource
|
||||
* @param Question $question The question being asked
|
||||
*
|
||||
* @return string|false The input received, false in case input could not be read
|
||||
*/
|
||||
private function readInput($inputStream, Question $question)
|
||||
{
|
||||
if (!$question->isMultiline()) {
|
||||
$cp = $this->setIOCodepage();
|
||||
$ret = fgets($inputStream, 4096);
|
||||
|
||||
return $this->resetIOCodepage($cp, $ret);
|
||||
}
|
||||
|
||||
$multiLineStreamReader = $this->cloneInputStream($inputStream);
|
||||
if (null === $multiLineStreamReader) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ret = '';
|
||||
$cp = $this->setIOCodepage();
|
||||
while (false !== ($char = fgetc($multiLineStreamReader))) {
|
||||
if (\PHP_EOL === "{$ret}{$char}") {
|
||||
break;
|
||||
}
|
||||
$ret .= $char;
|
||||
}
|
||||
|
||||
return $this->resetIOCodepage($cp, $ret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets console I/O to the host code page.
|
||||
*
|
||||
* @return int Previous code page in IBM/EBCDIC format
|
||||
*/
|
||||
private function setIOCodepage(): int
|
||||
{
|
||||
if (\function_exists('sapi_windows_cp_set')) {
|
||||
$cp = sapi_windows_cp_get();
|
||||
sapi_windows_cp_set(sapi_windows_cp_get('oem'));
|
||||
|
||||
return $cp;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets console I/O to the specified code page and converts the user input.
|
||||
*
|
||||
* @param string|false $input
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
private function resetIOCodepage(int $cp, $input)
|
||||
{
|
||||
if (0 !== $cp) {
|
||||
sapi_windows_cp_set($cp);
|
||||
|
||||
if (false !== $input && '' !== $input) {
|
||||
$input = sapi_windows_cp_conv(sapi_windows_cp_get('oem'), $cp, $input);
|
||||
}
|
||||
}
|
||||
|
||||
return self::$shell;
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones an input stream in order to act on one instance of the same
|
||||
* stream without affecting the other instance.
|
||||
*
|
||||
* @param resource $inputStream The handler resource
|
||||
*
|
||||
* @return resource|null The cloned resource, null in case it could not be cloned
|
||||
*/
|
||||
private function cloneInputStream($inputStream)
|
||||
{
|
||||
$streamMetaData = stream_get_meta_data($inputStream);
|
||||
$seekable = $streamMetaData['seekable'] ?? false;
|
||||
$mode = $streamMetaData['mode'] ?? 'rb';
|
||||
$uri = $streamMetaData['uri'] ?? null;
|
||||
|
||||
if (null === $uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cloneStream = fopen($uri, $mode);
|
||||
|
||||
// For seekable and writable streams, add all the same data to the
|
||||
// cloned stream and then seek to the same offset.
|
||||
if (true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'])) {
|
||||
$offset = ftell($inputStream);
|
||||
rewind($inputStream);
|
||||
stream_copy_to_stream($inputStream, $cloneStream);
|
||||
fseek($inputStream, $offset);
|
||||
fseek($cloneStream, $offset);
|
||||
}
|
||||
|
||||
return $cloneStream;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-1
@@ -33,6 +33,10 @@ class SymfonyQuestionHelper extends QuestionHelper
|
||||
$text = OutputFormatter::escapeTrailingBackslash($question->getQuestion());
|
||||
$default = $question->getDefault();
|
||||
|
||||
if ($question->isMultiline()) {
|
||||
$text .= sprintf(' (press %s to continue)', $this->getEofShortcut());
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case null === $default:
|
||||
$text = sprintf(' <info>%s</info>:', $text);
|
||||
@@ -58,7 +62,7 @@ class SymfonyQuestionHelper extends QuestionHelper
|
||||
|
||||
case $question instanceof ChoiceQuestion:
|
||||
$choices = $question->getChoices();
|
||||
$text = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, OutputFormatter::escape(isset($choices[$default]) ? $choices[$default] : $default));
|
||||
$text = sprintf(' <info>%s</info> [<comment>%s</comment>]:', $text, OutputFormatter::escape($choices[$default] ?? $default));
|
||||
|
||||
break;
|
||||
|
||||
@@ -93,4 +97,13 @@ class SymfonyQuestionHelper extends QuestionHelper
|
||||
|
||||
parent::writeError($output, $error);
|
||||
}
|
||||
|
||||
private function getEofShortcut(): string
|
||||
{
|
||||
if ('Windows' === \PHP_OS_FAMILY) {
|
||||
return '<comment>Ctrl+Z</comment> then <comment>Enter</comment>';
|
||||
}
|
||||
|
||||
return '<comment>Ctrl+D</comment>';
|
||||
}
|
||||
}
|
||||
|
||||
+61
-32
@@ -218,7 +218,7 @@ class Table
|
||||
public function setColumnMaxWidth(int $columnIndex, int $width): self
|
||||
{
|
||||
if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) {
|
||||
throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, \get_class($this->output->getFormatter())));
|
||||
throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, get_debug_type($this->output->getFormatter())));
|
||||
}
|
||||
|
||||
$this->columnMaxWidths[$columnIndex] = $width;
|
||||
@@ -362,6 +362,7 @@ class Table
|
||||
|
||||
$isHeader = !$this->horizontal;
|
||||
$isFirstRow = $this->horizontal;
|
||||
$hasTitle = (bool) $this->headerTitle;
|
||||
foreach ($rows as $row) {
|
||||
if ($divider === $row) {
|
||||
$isHeader = false;
|
||||
@@ -379,12 +380,13 @@ class Table
|
||||
}
|
||||
|
||||
if ($isHeader || $isFirstRow) {
|
||||
if ($isFirstRow) {
|
||||
$this->renderRowSeparator(self::SEPARATOR_TOP_BOTTOM);
|
||||
$isFirstRow = false;
|
||||
} else {
|
||||
$this->renderRowSeparator(self::SEPARATOR_TOP, $this->headerTitle, $this->style->getHeaderTitleFormat());
|
||||
}
|
||||
$this->renderRowSeparator(
|
||||
$isHeader ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM,
|
||||
$hasTitle ? $this->headerTitle : null,
|
||||
$hasTitle ? $this->style->getHeaderTitleFormat() : null
|
||||
);
|
||||
$isFirstRow = false;
|
||||
$hasTitle = false;
|
||||
}
|
||||
if ($this->horizontal) {
|
||||
$this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat());
|
||||
@@ -418,13 +420,13 @@ class Table
|
||||
|
||||
$crossings = $this->style->getCrossingChars();
|
||||
if (self::SEPARATOR_MID === $type) {
|
||||
list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[2], $crossings[8], $crossings[0], $crossings[4]];
|
||||
[$horizontal, $leftChar, $midChar, $rightChar] = [$borders[2], $crossings[8], $crossings[0], $crossings[4]];
|
||||
} elseif (self::SEPARATOR_TOP === $type) {
|
||||
list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[1], $crossings[2], $crossings[3]];
|
||||
[$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[1], $crossings[2], $crossings[3]];
|
||||
} elseif (self::SEPARATOR_TOP_BOTTOM === $type) {
|
||||
list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[9], $crossings[10], $crossings[11]];
|
||||
[$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[9], $crossings[10], $crossings[11]];
|
||||
} else {
|
||||
list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[7], $crossings[6], $crossings[5]];
|
||||
[$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[7], $crossings[6], $crossings[5]];
|
||||
}
|
||||
|
||||
$markup = $leftChar;
|
||||
@@ -434,15 +436,15 @@ class Table
|
||||
}
|
||||
|
||||
if (null !== $title) {
|
||||
$titleLength = Helper::strlenWithoutDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title));
|
||||
$markupLength = Helper::strlen($markup);
|
||||
$titleLength = Helper::width(Helper::removeDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title)));
|
||||
$markupLength = Helper::width($markup);
|
||||
if ($titleLength > $limit = $markupLength - 4) {
|
||||
$titleLength = $limit;
|
||||
$formatLength = Helper::strlenWithoutDecoration($formatter, sprintf($titleFormat, ''));
|
||||
$formatLength = Helper::width(Helper::removeDecoration($formatter, sprintf($titleFormat, '')));
|
||||
$formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...');
|
||||
}
|
||||
|
||||
$titleStart = ($markupLength - $titleLength) / 2;
|
||||
$titleStart = intdiv($markupLength - $titleLength, 2);
|
||||
if (false === mb_detect_encoding($markup, null, true)) {
|
||||
$markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength);
|
||||
} else {
|
||||
@@ -491,7 +493,7 @@ class Table
|
||||
*/
|
||||
private function renderCell(array $row, int $column, string $cellFormat): string
|
||||
{
|
||||
$cell = isset($row[$column]) ? $row[$column] : '';
|
||||
$cell = $row[$column] ?? '';
|
||||
$width = $this->effectiveColumnWidths[$column];
|
||||
if ($cell instanceof TableCell && $cell->getColspan() > 1) {
|
||||
// add the width of the following columns(numbers of colspan).
|
||||
@@ -511,10 +513,33 @@ class Table
|
||||
return sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width));
|
||||
}
|
||||
|
||||
$width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
|
||||
$width += Helper::length($cell) - Helper::length(Helper::removeDecoration($this->output->getFormatter(), $cell));
|
||||
$content = sprintf($style->getCellRowContentFormat(), $cell);
|
||||
|
||||
return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $style->getPadType()));
|
||||
$padType = $style->getPadType();
|
||||
if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) {
|
||||
$isNotStyledByTag = !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell);
|
||||
if ($isNotStyledByTag) {
|
||||
$cellFormat = $cell->getStyle()->getCellFormat();
|
||||
if (!\is_string($cellFormat)) {
|
||||
$tag = http_build_query($cell->getStyle()->getTagOptions(), '', ';');
|
||||
$cellFormat = '<'.$tag.'>%s</>';
|
||||
}
|
||||
|
||||
if (strstr($content, '</>')) {
|
||||
$content = str_replace('</>', '', $content);
|
||||
$width -= 3;
|
||||
}
|
||||
if (strstr($content, '<fg=default;bg=default>')) {
|
||||
$content = str_replace('<fg=default;bg=default>', '', $content);
|
||||
$width -= \strlen('<fg=default;bg=default>');
|
||||
}
|
||||
}
|
||||
|
||||
$padType = $cell->getStyle()->getPadByAlign();
|
||||
}
|
||||
|
||||
return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -546,10 +571,10 @@ class Table
|
||||
foreach ($rows[$rowKey] as $column => $cell) {
|
||||
$colspan = $cell instanceof TableCell ? $cell->getColspan() : 1;
|
||||
|
||||
if (isset($this->columnMaxWidths[$column]) && Helper::strlenWithoutDecoration($formatter, $cell) > $this->columnMaxWidths[$column]) {
|
||||
if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) {
|
||||
$cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
|
||||
}
|
||||
if (!strstr($cell, "\n")) {
|
||||
if (!strstr($cell ?? '', "\n")) {
|
||||
continue;
|
||||
}
|
||||
$escaped = implode("\n", array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode("\n", $cell)));
|
||||
@@ -562,6 +587,9 @@ class Table
|
||||
if (0 === $lineKey) {
|
||||
$rows[$rowKey][$column] = $line;
|
||||
} else {
|
||||
if (!\array_key_exists($rowKey, $unmergedRows) || !\array_key_exists($lineKey, $unmergedRows[$rowKey])) {
|
||||
$unmergedRows[$rowKey][$lineKey] = $this->copyRow($rows, $rowKey);
|
||||
}
|
||||
$unmergedRows[$rowKey][$lineKey][$column] = $line;
|
||||
}
|
||||
}
|
||||
@@ -570,11 +598,11 @@ class Table
|
||||
|
||||
return new TableRows(function () use ($rows, $unmergedRows): \Traversable {
|
||||
foreach ($rows as $rowKey => $row) {
|
||||
yield $this->fillCells($row);
|
||||
yield $row instanceof TableSeparator ? $row : $this->fillCells($row);
|
||||
|
||||
if (isset($unmergedRows[$rowKey])) {
|
||||
foreach ($unmergedRows[$rowKey] as $row) {
|
||||
yield $row;
|
||||
yield $row instanceof TableSeparator ? $row : $this->fillCells($row);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -606,7 +634,7 @@ class Table
|
||||
$unmergedRows = [];
|
||||
foreach ($rows[$line] as $column => $cell) {
|
||||
if (null !== $cell && !$cell instanceof TableCell && !is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) {
|
||||
throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', \gettype($cell)));
|
||||
throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell)));
|
||||
}
|
||||
if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
|
||||
$nbLines = $cell->getRowspan() - 1;
|
||||
@@ -615,15 +643,15 @@ class Table
|
||||
$lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
|
||||
$nbLines = \count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines;
|
||||
|
||||
$rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan()]);
|
||||
$rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
|
||||
unset($lines[0]);
|
||||
}
|
||||
|
||||
// create a two dimensional array (rowspan x colspan)
|
||||
$unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows);
|
||||
foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
|
||||
$value = isset($lines[$unmergedRowKey - $line]) ? $lines[$unmergedRowKey - $line] : '';
|
||||
$unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan()]);
|
||||
$value = $lines[$unmergedRowKey - $line] ?? '';
|
||||
$unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
|
||||
if ($nbLines === $unmergedRowKey - $line) {
|
||||
break;
|
||||
}
|
||||
@@ -655,9 +683,10 @@ class Table
|
||||
/**
|
||||
* fill cells for a row that contains colspan > 1.
|
||||
*/
|
||||
private function fillCells($row)
|
||||
private function fillCells(iterable $row)
|
||||
{
|
||||
$newRow = [];
|
||||
|
||||
foreach ($row as $column => $cell) {
|
||||
$newRow[] = $cell;
|
||||
if ($cell instanceof TableCell && $cell->getColspan() > 1) {
|
||||
@@ -728,7 +757,7 @@ class Table
|
||||
foreach ($row as $i => $cell) {
|
||||
if ($cell instanceof TableCell) {
|
||||
$textContent = Helper::removeDecoration($this->output->getFormatter(), $cell);
|
||||
$textLength = Helper::strlen($textContent);
|
||||
$textLength = Helper::width($textContent);
|
||||
if ($textLength > 0) {
|
||||
$contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan()));
|
||||
foreach ($contentColumns as $position => $content) {
|
||||
@@ -741,13 +770,13 @@ class Table
|
||||
$lengths[] = $this->getCellWidth($row, $column);
|
||||
}
|
||||
|
||||
$this->effectiveColumnWidths[$column] = max($lengths) + Helper::strlen($this->style->getCellRowContentFormat()) - 2;
|
||||
$this->effectiveColumnWidths[$column] = max($lengths) + Helper::width($this->style->getCellRowContentFormat()) - 2;
|
||||
}
|
||||
}
|
||||
|
||||
private function getColumnSeparatorWidth(): int
|
||||
{
|
||||
return Helper::strlen(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
|
||||
return Helper::width(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
|
||||
}
|
||||
|
||||
private function getCellWidth(array $row, int $column): int
|
||||
@@ -756,10 +785,10 @@ class Table
|
||||
|
||||
if (isset($row[$column])) {
|
||||
$cell = $row[$column];
|
||||
$cellWidth = Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
|
||||
$cellWidth = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $cell));
|
||||
}
|
||||
|
||||
$columnWidth = isset($this->columnWidths[$column]) ? $this->columnWidths[$column] : 0;
|
||||
$columnWidth = $this->columnWidths[$column] ?? 0;
|
||||
$cellWidth = max($cellWidth, $columnWidth);
|
||||
|
||||
return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth;
|
||||
|
||||
+10
@@ -22,6 +22,7 @@ class TableCell
|
||||
private $options = [
|
||||
'rowspan' => 1,
|
||||
'colspan' => 1,
|
||||
'style' => null,
|
||||
];
|
||||
|
||||
public function __construct(string $value = '', array $options = [])
|
||||
@@ -33,6 +34,10 @@ class TableCell
|
||||
throw new InvalidArgumentException(sprintf('The TableCell does not support the following options: \'%s\'.', implode('\', \'', $diff)));
|
||||
}
|
||||
|
||||
if (isset($options['style']) && !$options['style'] instanceof TableCellStyle) {
|
||||
throw new InvalidArgumentException('The style option must be an instance of "TableCellStyle".');
|
||||
}
|
||||
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
|
||||
@@ -65,4 +70,9 @@ class TableCell
|
||||
{
|
||||
return (int) $this->options['rowspan'];
|
||||
}
|
||||
|
||||
public function getStyle(): ?TableCellStyle
|
||||
{
|
||||
return $this->options['style'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?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\Helper;
|
||||
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @author Yewhen Khoptynskyi <khoptynskyi@gmail.com>
|
||||
*/
|
||||
class TableCellStyle
|
||||
{
|
||||
public const DEFAULT_ALIGN = 'left';
|
||||
|
||||
private $options = [
|
||||
'fg' => 'default',
|
||||
'bg' => 'default',
|
||||
'options' => null,
|
||||
'align' => self::DEFAULT_ALIGN,
|
||||
'cellFormat' => null,
|
||||
];
|
||||
|
||||
private $tagOptions = [
|
||||
'fg',
|
||||
'bg',
|
||||
'options',
|
||||
];
|
||||
|
||||
private $alignMap = [
|
||||
'left' => \STR_PAD_RIGHT,
|
||||
'center' => \STR_PAD_BOTH,
|
||||
'right' => \STR_PAD_LEFT,
|
||||
];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
if ($diff = array_diff(array_keys($options), array_keys($this->options))) {
|
||||
throw new InvalidArgumentException(sprintf('The TableCellStyle does not support the following options: \'%s\'.', implode('\', \'', $diff)));
|
||||
}
|
||||
|
||||
if (isset($options['align']) && !\array_key_exists($options['align'], $this->alignMap)) {
|
||||
throw new InvalidArgumentException(sprintf('Wrong align value. Value must be following: \'%s\'.', implode('\', \'', array_keys($this->alignMap))));
|
||||
}
|
||||
|
||||
$this->options = array_merge($this->options, $options);
|
||||
}
|
||||
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets options we need for tag for example fg, bg.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTagOptions()
|
||||
{
|
||||
return array_filter(
|
||||
$this->getOptions(),
|
||||
function ($key) {
|
||||
return \in_array($key, $this->tagOptions) && isset($this->options[$key]);
|
||||
},
|
||||
\ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
public function getPadByAlign()
|
||||
{
|
||||
return $this->alignMap[$this->getOptions()['align']];
|
||||
}
|
||||
|
||||
public function getCellFormat(): ?string
|
||||
{
|
||||
return $this->getOptions()['cellFormat'];
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -46,7 +46,7 @@ class TableStyle
|
||||
private $cellRowFormat = '%s';
|
||||
private $cellRowContentFormat = ' %s ';
|
||||
private $borderFormat = '%s';
|
||||
private $padType = STR_PAD_RIGHT;
|
||||
private $padType = \STR_PAD_RIGHT;
|
||||
|
||||
/**
|
||||
* Sets padding character, used for cell padding.
|
||||
@@ -319,7 +319,7 @@ class TableStyle
|
||||
*/
|
||||
public function setPadType(int $padType)
|
||||
{
|
||||
if (!\in_array($padType, [STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH], true)) {
|
||||
if (!\in_array($padType, [\STR_PAD_LEFT, \STR_PAD_RIGHT, \STR_PAD_BOTH], true)) {
|
||||
throw new InvalidArgumentException('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).');
|
||||
}
|
||||
|
||||
|
||||
+35
-13
@@ -45,9 +45,7 @@ class ArgvInput extends Input
|
||||
|
||||
public function __construct(array $argv = null, InputDefinition $definition = null)
|
||||
{
|
||||
if (null === $argv) {
|
||||
$argv = $_SERVER['argv'];
|
||||
}
|
||||
$argv = $argv ?? $_SERVER['argv'] ?? [];
|
||||
|
||||
// strip the application name
|
||||
array_shift($argv);
|
||||
@@ -74,7 +72,7 @@ class ArgvInput extends Input
|
||||
$this->parseArgument($token);
|
||||
} elseif ($parseOptions && '--' == $token) {
|
||||
$parseOptions = false;
|
||||
} elseif ($parseOptions && 0 === strpos($token, '--')) {
|
||||
} elseif ($parseOptions && str_starts_with($token, '--')) {
|
||||
$this->parseLongOption($token);
|
||||
} elseif ($parseOptions && '-' === $token[0] && '-' !== $token) {
|
||||
$this->parseShortOption($token);
|
||||
@@ -167,11 +165,25 @@ class ArgvInput extends Input
|
||||
// unexpected argument
|
||||
} else {
|
||||
$all = $this->definition->getArguments();
|
||||
if (\count($all)) {
|
||||
throw new RuntimeException(sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all))));
|
||||
$symfonyCommandName = null;
|
||||
if (($inputArgument = $all[$key = array_key_first($all)] ?? null) && 'command' === $inputArgument->getName()) {
|
||||
$symfonyCommandName = $this->arguments['command'] ?? null;
|
||||
unset($all[$key]);
|
||||
}
|
||||
|
||||
throw new RuntimeException(sprintf('No arguments expected, got "%s".', $token));
|
||||
if (\count($all)) {
|
||||
if ($symfonyCommandName) {
|
||||
$message = sprintf('Too many arguments to "%s" command, expected arguments "%s".', $symfonyCommandName, implode('" "', array_keys($all)));
|
||||
} else {
|
||||
$message = sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all)));
|
||||
}
|
||||
} elseif ($symfonyCommandName) {
|
||||
$message = sprintf('No arguments expected for "%s" command, got "%s".', $symfonyCommandName, $token);
|
||||
} else {
|
||||
$message = sprintf('No arguments expected, got "%s".', $token);
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +209,17 @@ class ArgvInput extends Input
|
||||
private function addLongOption(string $name, $value)
|
||||
{
|
||||
if (!$this->definition->hasOption($name)) {
|
||||
throw new RuntimeException(sprintf('The "--%s" option does not exist.', $name));
|
||||
if (!$this->definition->hasNegation($name)) {
|
||||
throw new RuntimeException(sprintf('The "--%s" option does not exist.', $name));
|
||||
}
|
||||
|
||||
$optionName = $this->definition->negationToName($name);
|
||||
if (null !== $value) {
|
||||
throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name));
|
||||
}
|
||||
$this->options[$optionName] = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$option = $this->definition->getOption($name);
|
||||
@@ -242,7 +264,7 @@ class ArgvInput extends Input
|
||||
$isOption = false;
|
||||
foreach ($this->tokens as $i => $token) {
|
||||
if ($token && '-' === $token[0]) {
|
||||
if (false !== strpos($token, '=') || !isset($this->tokens[$i + 1])) {
|
||||
if (str_contains($token, '=') || !isset($this->tokens[$i + 1])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -284,8 +306,8 @@ class ArgvInput extends Input
|
||||
// Options with values:
|
||||
// For long options, test for '--option=' at beginning
|
||||
// For short options, test for '-o' at beginning
|
||||
$leading = 0 === strpos($value, '--') ? $value.'=' : $value;
|
||||
if ($token === $value || '' !== $leading && 0 === strpos($token, $leading)) {
|
||||
$leading = str_starts_with($value, '--') ? $value.'=' : $value;
|
||||
if ($token === $value || '' !== $leading && str_starts_with($token, $leading)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -315,8 +337,8 @@ class ArgvInput extends Input
|
||||
// Options with values:
|
||||
// For long options, test for '--option=' at beginning
|
||||
// For short options, test for '-o' at beginning
|
||||
$leading = 0 === strpos($value, '--') ? $value.'=' : $value;
|
||||
if ('' !== $leading && 0 === strpos($token, $leading)) {
|
||||
$leading = str_starts_with($value, '--') ? $value.'=' : $value;
|
||||
if ('' !== $leading && str_starts_with($token, $leading)) {
|
||||
return substr($token, \strlen($leading));
|
||||
}
|
||||
}
|
||||
|
||||
+13
-5
@@ -108,12 +108,13 @@ class ArrayInput extends Input
|
||||
$params = [];
|
||||
foreach ($this->parameters as $param => $val) {
|
||||
if ($param && \is_string($param) && '-' === $param[0]) {
|
||||
$glue = ('-' === $param[1]) ? '=' : ' ';
|
||||
if (\is_array($val)) {
|
||||
foreach ($val as $v) {
|
||||
$params[] = $param.('' != $v ? '='.$this->escapeToken($v) : '');
|
||||
$params[] = $param.('' != $v ? $glue.$this->escapeToken($v) : '');
|
||||
}
|
||||
} else {
|
||||
$params[] = $param.('' != $val ? '='.$this->escapeToken($val) : '');
|
||||
$params[] = $param.('' != $val ? $glue.$this->escapeToken($val) : '');
|
||||
}
|
||||
} else {
|
||||
$params[] = \is_array($val) ? implode(' ', array_map([$this, 'escapeToken'], $val)) : $this->escapeToken($val);
|
||||
@@ -132,9 +133,9 @@ class ArrayInput extends Input
|
||||
if ('--' === $key) {
|
||||
return;
|
||||
}
|
||||
if (0 === strpos($key, '--')) {
|
||||
if (str_starts_with($key, '--')) {
|
||||
$this->addLongOption(substr($key, 2), $value);
|
||||
} elseif (0 === strpos($key, '-')) {
|
||||
} elseif (str_starts_with($key, '-')) {
|
||||
$this->addShortOption(substr($key, 1), $value);
|
||||
} else {
|
||||
$this->addArgument($key, $value);
|
||||
@@ -165,7 +166,14 @@ class ArrayInput extends Input
|
||||
private function addLongOption(string $name, $value)
|
||||
{
|
||||
if (!$this->definition->hasOption($name)) {
|
||||
throw new InvalidOptionException(sprintf('The "--%s" option does not exist.', $name));
|
||||
if (!$this->definition->hasNegation($name)) {
|
||||
throw new InvalidOptionException(sprintf('The "--%s" option does not exist.', $name));
|
||||
}
|
||||
|
||||
$optionName = $this->definition->negationToName($name);
|
||||
$this->options[$optionName] = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$option = $this->definition->getOption($name);
|
||||
|
||||
+16
-4
@@ -110,7 +110,7 @@ abstract class Input implements InputInterface, StreamableInputInterface
|
||||
throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
|
||||
}
|
||||
|
||||
return isset($this->arguments[$name]) ? $this->arguments[$name] : $this->definition->getArgument($name)->getDefault();
|
||||
return $this->arguments[$name] ?? $this->definition->getArgument($name)->getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +128,7 @@ abstract class Input implements InputInterface, StreamableInputInterface
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function hasArgument($name)
|
||||
public function hasArgument(string $name)
|
||||
{
|
||||
return $this->definition->hasArgument($name);
|
||||
}
|
||||
@@ -146,6 +146,14 @@ abstract class Input implements InputInterface, StreamableInputInterface
|
||||
*/
|
||||
public function getOption(string $name)
|
||||
{
|
||||
if ($this->definition->hasNegation($name)) {
|
||||
if (null === $value = $this->getOption($this->definition->negationToName($name))) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return !$value;
|
||||
}
|
||||
|
||||
if (!$this->definition->hasOption($name)) {
|
||||
throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
|
||||
}
|
||||
@@ -158,7 +166,11 @@ abstract class Input implements InputInterface, StreamableInputInterface
|
||||
*/
|
||||
public function setOption(string $name, $value)
|
||||
{
|
||||
if (!$this->definition->hasOption($name)) {
|
||||
if ($this->definition->hasNegation($name)) {
|
||||
$this->options[$this->definition->negationToName($name)] = !$value;
|
||||
|
||||
return;
|
||||
} elseif (!$this->definition->hasOption($name)) {
|
||||
throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
|
||||
}
|
||||
|
||||
@@ -170,7 +182,7 @@ abstract class Input implements InputInterface, StreamableInputInterface
|
||||
*/
|
||||
public function hasOption(string $name)
|
||||
{
|
||||
return $this->definition->hasOption($name);
|
||||
return $this->definition->hasOption($name) || $this->definition->hasNegation($name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+9
-9
@@ -21,9 +21,9 @@ use Symfony\Component\Console\Exception\LogicException;
|
||||
*/
|
||||
class InputArgument
|
||||
{
|
||||
const REQUIRED = 1;
|
||||
const OPTIONAL = 2;
|
||||
const IS_ARRAY = 4;
|
||||
public const REQUIRED = 1;
|
||||
public const OPTIONAL = 2;
|
||||
public const IS_ARRAY = 4;
|
||||
|
||||
private $name;
|
||||
private $mode;
|
||||
@@ -31,10 +31,10 @@ class InputArgument
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @param string $name The argument name
|
||||
* @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL
|
||||
* @param string $description A description text
|
||||
* @param string|string[]|null $default The default value (for self::OPTIONAL mode only)
|
||||
* @param string $name The argument name
|
||||
* @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL
|
||||
* @param string $description A description text
|
||||
* @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only)
|
||||
*
|
||||
* @throws InvalidArgumentException When argument mode is not valid
|
||||
*/
|
||||
@@ -86,7 +86,7 @@ class InputArgument
|
||||
/**
|
||||
* Sets the default value.
|
||||
*
|
||||
* @param string|string[]|null $default The default value
|
||||
* @param string|bool|int|float|array|null $default
|
||||
*
|
||||
* @throws LogicException When incorrect default value is given
|
||||
*/
|
||||
@@ -110,7 +110,7 @@ class InputArgument
|
||||
/**
|
||||
* Returns the default value.
|
||||
*
|
||||
* @return string|string[]|null The default value
|
||||
* @return string|bool|int|float|array|null
|
||||
*/
|
||||
public function getDefault()
|
||||
{
|
||||
|
||||
+52
-18
@@ -30,9 +30,10 @@ class InputDefinition
|
||||
{
|
||||
private $arguments;
|
||||
private $requiredCount;
|
||||
private $hasAnArrayArgument = false;
|
||||
private $hasOptional;
|
||||
private $lastArrayArgument;
|
||||
private $lastOptionalArgument;
|
||||
private $options;
|
||||
private $negations;
|
||||
private $shortcuts;
|
||||
|
||||
/**
|
||||
@@ -71,8 +72,8 @@ class InputDefinition
|
||||
{
|
||||
$this->arguments = [];
|
||||
$this->requiredCount = 0;
|
||||
$this->hasOptional = false;
|
||||
$this->hasAnArrayArgument = false;
|
||||
$this->lastOptionalArgument = null;
|
||||
$this->lastArrayArgument = null;
|
||||
$this->addArguments($arguments);
|
||||
}
|
||||
|
||||
@@ -99,22 +100,22 @@ class InputDefinition
|
||||
throw new LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName()));
|
||||
}
|
||||
|
||||
if ($this->hasAnArrayArgument) {
|
||||
throw new LogicException('Cannot add an argument after an array argument.');
|
||||
if (null !== $this->lastArrayArgument) {
|
||||
throw new LogicException(sprintf('Cannot add a required argument "%s" after an array argument "%s".', $argument->getName(), $this->lastArrayArgument->getName()));
|
||||
}
|
||||
|
||||
if ($argument->isRequired() && $this->hasOptional) {
|
||||
throw new LogicException('Cannot add a required argument after an optional one.');
|
||||
if ($argument->isRequired() && null !== $this->lastOptionalArgument) {
|
||||
throw new LogicException(sprintf('Cannot add a required argument "%s" after an optional one "%s".', $argument->getName(), $this->lastOptionalArgument->getName()));
|
||||
}
|
||||
|
||||
if ($argument->isArray()) {
|
||||
$this->hasAnArrayArgument = true;
|
||||
$this->lastArrayArgument = $argument;
|
||||
}
|
||||
|
||||
if ($argument->isRequired()) {
|
||||
++$this->requiredCount;
|
||||
} else {
|
||||
$this->hasOptional = true;
|
||||
$this->lastOptionalArgument = $argument;
|
||||
}
|
||||
|
||||
$this->arguments[$argument->getName()] = $argument;
|
||||
@@ -171,7 +172,7 @@ class InputDefinition
|
||||
*/
|
||||
public function getArgumentCount()
|
||||
{
|
||||
return $this->hasAnArrayArgument ? PHP_INT_MAX : \count($this->arguments);
|
||||
return null !== $this->lastArrayArgument ? \PHP_INT_MAX : \count($this->arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,9 +186,7 @@ class InputDefinition
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default values.
|
||||
*
|
||||
* @return array An array of default values
|
||||
* @return array<string|bool|int|float|array|null>
|
||||
*/
|
||||
public function getArgumentDefaults()
|
||||
{
|
||||
@@ -208,6 +207,7 @@ class InputDefinition
|
||||
{
|
||||
$this->options = [];
|
||||
$this->shortcuts = [];
|
||||
$this->negations = [];
|
||||
$this->addOptions($options);
|
||||
}
|
||||
|
||||
@@ -231,6 +231,9 @@ class InputDefinition
|
||||
if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) {
|
||||
throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName()));
|
||||
}
|
||||
if (isset($this->negations[$option->getName()])) {
|
||||
throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName()));
|
||||
}
|
||||
|
||||
if ($option->getShortcut()) {
|
||||
foreach (explode('|', $option->getShortcut()) as $shortcut) {
|
||||
@@ -246,6 +249,14 @@ class InputDefinition
|
||||
$this->shortcuts[$shortcut] = $option->getName();
|
||||
}
|
||||
}
|
||||
|
||||
if ($option->isNegatable()) {
|
||||
$negatedName = 'no-'.$option->getName();
|
||||
if (isset($this->options[$negatedName])) {
|
||||
throw new LogicException(sprintf('An option named "%s" already exists.', $negatedName));
|
||||
}
|
||||
$this->negations[$negatedName] = $option->getName();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,6 +308,14 @@ class InputDefinition
|
||||
return isset($this->shortcuts[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if an InputOption object exists by negated name.
|
||||
*/
|
||||
public function hasNegation(string $name): bool
|
||||
{
|
||||
return isset($this->negations[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an InputOption by shortcut.
|
||||
*
|
||||
@@ -308,9 +327,7 @@ class InputDefinition
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of default values.
|
||||
*
|
||||
* @return array An array of all default values
|
||||
* @return array<string|bool|int|float|array|null>
|
||||
*/
|
||||
public function getOptionDefaults()
|
||||
{
|
||||
@@ -338,6 +355,22 @@ class InputDefinition
|
||||
return $this->shortcuts[$shortcut];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the InputOption name given a negation.
|
||||
*
|
||||
* @throws InvalidArgumentException When option given does not exist
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function negationToName(string $negation): string
|
||||
{
|
||||
if (!isset($this->negations[$negation])) {
|
||||
throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $negation));
|
||||
}
|
||||
|
||||
return $this->negations[$negation];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the synopsis.
|
||||
*
|
||||
@@ -362,7 +395,8 @@ class InputDefinition
|
||||
}
|
||||
|
||||
$shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : '';
|
||||
$elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value);
|
||||
$negation = $option->isNegatable() ? sprintf('|--no-%s', $option->getName()) : '';
|
||||
$elements[] = sprintf('[%s--%s%s%s]', $shortcut, $option->getName(), $value, $negation);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-12
@@ -51,9 +51,9 @@ interface InputInterface
|
||||
* Does not necessarily return the correct result for short options
|
||||
* when multiple flags are combined in the same option.
|
||||
*
|
||||
* @param string|array $values The value(s) to look for in the raw parameters (can be an array)
|
||||
* @param mixed $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
|
||||
* @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 The option value
|
||||
*/
|
||||
@@ -76,14 +76,14 @@ interface InputInterface
|
||||
/**
|
||||
* Returns all the given arguments merged with the default values.
|
||||
*
|
||||
* @return array
|
||||
* @return array<string|bool|int|float|array|null>
|
||||
*/
|
||||
public function getArguments();
|
||||
|
||||
/**
|
||||
* Returns the argument value for a given argument name.
|
||||
*
|
||||
* @return string|string[]|null The argument value
|
||||
* @return mixed
|
||||
*
|
||||
* @throws InvalidArgumentException When argument given doesn't exist
|
||||
*/
|
||||
@@ -92,7 +92,7 @@ interface InputInterface
|
||||
/**
|
||||
* Sets an argument value by name.
|
||||
*
|
||||
* @param string|string[]|null $value The argument value
|
||||
* @param mixed $value The argument value
|
||||
*
|
||||
* @throws InvalidArgumentException When argument given doesn't exist
|
||||
*/
|
||||
@@ -101,23 +101,21 @@ interface InputInterface
|
||||
/**
|
||||
* Returns true if an InputArgument object exists by name or position.
|
||||
*
|
||||
* @param string|int $name The InputArgument name or position
|
||||
*
|
||||
* @return bool true if the InputArgument object exists, false otherwise
|
||||
*/
|
||||
public function hasArgument($name);
|
||||
public function hasArgument(string $name);
|
||||
|
||||
/**
|
||||
* Returns all the given options merged with the default values.
|
||||
*
|
||||
* @return array
|
||||
* @return array<string|bool|int|float|array|null>
|
||||
*/
|
||||
public function getOptions();
|
||||
|
||||
/**
|
||||
* Returns the option value for a given option name.
|
||||
*
|
||||
* @return string|string[]|bool|null The option value
|
||||
* @return mixed
|
||||
*
|
||||
* @throws InvalidArgumentException When option given doesn't exist
|
||||
*/
|
||||
@@ -126,7 +124,7 @@ interface InputInterface
|
||||
/**
|
||||
* Sets an option value by name.
|
||||
*
|
||||
* @param string|string[]|bool|null $value The option value
|
||||
* @param mixed $value The option value
|
||||
*
|
||||
* @throws InvalidArgumentException When option given doesn't exist
|
||||
*/
|
||||
|
||||
+43
-18
@@ -21,10 +21,30 @@ use Symfony\Component\Console\Exception\LogicException;
|
||||
*/
|
||||
class InputOption
|
||||
{
|
||||
const VALUE_NONE = 1;
|
||||
const VALUE_REQUIRED = 2;
|
||||
const VALUE_OPTIONAL = 4;
|
||||
const VALUE_IS_ARRAY = 8;
|
||||
/**
|
||||
* Do not accept input for the option (e.g. --yell). This is the default behavior of options.
|
||||
*/
|
||||
public const VALUE_NONE = 1;
|
||||
|
||||
/**
|
||||
* A value must be passed when the option is used (e.g. --iterations=5 or -i5).
|
||||
*/
|
||||
public const VALUE_REQUIRED = 2;
|
||||
|
||||
/**
|
||||
* The option may or may not have a value (e.g. --yell or --yell=loud).
|
||||
*/
|
||||
public const VALUE_OPTIONAL = 4;
|
||||
|
||||
/**
|
||||
* The option accepts multiple values (e.g. --dir=/foo --dir=/bar).
|
||||
*/
|
||||
public const VALUE_IS_ARRAY = 8;
|
||||
|
||||
/**
|
||||
* The option may have either positive or negative value (e.g. --ansi or --no-ansi).
|
||||
*/
|
||||
public const VALUE_NEGATABLE = 16;
|
||||
|
||||
private $name;
|
||||
private $shortcut;
|
||||
@@ -33,17 +53,17 @@ class InputOption
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @param string $name The option name
|
||||
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
|
||||
* @param int|null $mode The option mode: One of the VALUE_* constants
|
||||
* @param string $description A description text
|
||||
* @param string|string[]|int|bool|null $default The default value (must be null for self::VALUE_NONE)
|
||||
* @param string $name The option name
|
||||
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
|
||||
* @param int|null $mode The option mode: One of the VALUE_* constants
|
||||
* @param string $description A description text
|
||||
* @param string|bool|int|float|array|null $default The default value (must be null for self::VALUE_NONE)
|
||||
*
|
||||
* @throws InvalidArgumentException If option mode is invalid or incompatible
|
||||
*/
|
||||
public function __construct(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null)
|
||||
{
|
||||
if (0 === strpos($name, '--')) {
|
||||
if (str_starts_with($name, '--')) {
|
||||
$name = substr($name, 2);
|
||||
}
|
||||
|
||||
@@ -70,7 +90,7 @@ class InputOption
|
||||
|
||||
if (null === $mode) {
|
||||
$mode = self::VALUE_NONE;
|
||||
} elseif ($mode > 15 || $mode < 1) {
|
||||
} elseif ($mode >= (self::VALUE_NEGATABLE << 1) || $mode < 1) {
|
||||
throw new InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode));
|
||||
}
|
||||
|
||||
@@ -82,6 +102,9 @@ class InputOption
|
||||
if ($this->isArray() && !$this->acceptValue()) {
|
||||
throw new InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.');
|
||||
}
|
||||
if ($this->isNegatable() && $this->acceptValue()) {
|
||||
throw new InvalidArgumentException('Impossible to have an option mode VALUE_NEGATABLE if the option also accepts a value.');
|
||||
}
|
||||
|
||||
$this->setDefault($default);
|
||||
}
|
||||
@@ -146,12 +169,13 @@ class InputOption
|
||||
return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode);
|
||||
}
|
||||
|
||||
public function isNegatable(): bool
|
||||
{
|
||||
return self::VALUE_NEGATABLE === (self::VALUE_NEGATABLE & $this->mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default value.
|
||||
*
|
||||
* @param string|string[]|int|bool|null $default The default value
|
||||
*
|
||||
* @throws LogicException When incorrect default value is given
|
||||
* @param string|bool|int|float|array|null $default
|
||||
*/
|
||||
public function setDefault($default = null)
|
||||
{
|
||||
@@ -167,13 +191,13 @@ class InputOption
|
||||
}
|
||||
}
|
||||
|
||||
$this->default = $this->acceptValue() ? $default : false;
|
||||
$this->default = $this->acceptValue() || $this->isNegatable() ? $default : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default value.
|
||||
*
|
||||
* @return string|string[]|int|bool|null The default value
|
||||
* @return string|bool|int|float|array|null
|
||||
*/
|
||||
public function getDefault()
|
||||
{
|
||||
@@ -200,6 +224,7 @@ class InputOption
|
||||
return $option->getName() === $this->getName()
|
||||
&& $option->getShortcut() === $this->getShortcut()
|
||||
&& $option->getDefault() === $this->getDefault()
|
||||
&& $option->isNegatable() === $this->isNegatable()
|
||||
&& $option->isArray() === $this->isArray()
|
||||
&& $option->isValueRequired() === $this->isValueRequired()
|
||||
&& $option->isValueOptional() === $this->isValueOptional()
|
||||
|
||||
+8
-8
@@ -24,8 +24,8 @@ use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
*/
|
||||
class StringInput extends ArgvInput
|
||||
{
|
||||
const REGEX_STRING = '([^\s]+?)(?:\s|(?<!\\\\)"|(?<!\\\\)\'|$)';
|
||||
const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')';
|
||||
public const REGEX_STRING = '([^\s]+?)(?:\s|(?<!\\\\)"|(?<!\\\\)\'|$)';
|
||||
public const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')';
|
||||
|
||||
/**
|
||||
* @param string $input A string representing the parameters from the CLI
|
||||
@@ -48,12 +48,12 @@ class StringInput extends ArgvInput
|
||||
$length = \strlen($input);
|
||||
$cursor = 0;
|
||||
while ($cursor < $length) {
|
||||
if (preg_match('/\s+/A', $input, $match, null, $cursor)) {
|
||||
} elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, null, $cursor)) {
|
||||
$tokens[] = $match[1].$match[2].stripcslashes(str_replace(['"\'', '\'"', '\'\'', '""'], '', substr($match[3], 1, \strlen($match[3]) - 2)));
|
||||
} elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, null, $cursor)) {
|
||||
$tokens[] = stripcslashes(substr($match[0], 1, \strlen($match[0]) - 2));
|
||||
} elseif (preg_match('/'.self::REGEX_STRING.'/A', $input, $match, null, $cursor)) {
|
||||
if (preg_match('/\s+/A', $input, $match, 0, $cursor)) {
|
||||
} elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, 0, $cursor)) {
|
||||
$tokens[] = $match[1].$match[2].stripcslashes(str_replace(['"\'', '\'"', '\'\'', '""'], '', substr($match[3], 1, -1)));
|
||||
} elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, 0, $cursor)) {
|
||||
$tokens[] = stripcslashes(substr($match[0], 1, -1));
|
||||
} elseif (preg_match('/'.self::REGEX_STRING.'/A', $input, $match, 0, $cursor)) {
|
||||
$tokens[] = stripcslashes($match[1]);
|
||||
} else {
|
||||
// should never happen
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2004-2020 Fabien Potencier
|
||||
Copyright (c) 2004-2021 Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
+3
-3
@@ -26,8 +26,8 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
*/
|
||||
class ConsoleLogger extends AbstractLogger
|
||||
{
|
||||
const INFO = 'info';
|
||||
const ERROR = 'error';
|
||||
public const INFO = 'info';
|
||||
public const ERROR = 'error';
|
||||
|
||||
private $output;
|
||||
private $verbosityLevelMap = [
|
||||
@@ -104,7 +104,7 @@ class ConsoleLogger extends AbstractLogger
|
||||
*/
|
||||
private function interpolate(string $message, array $context): string
|
||||
{
|
||||
if (false === strpos($message, '{')) {
|
||||
if (!str_contains($message, '{')) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ class BufferedOutput extends Output
|
||||
$this->buffer .= $message;
|
||||
|
||||
if ($newline) {
|
||||
$this->buffer .= PHP_EOL;
|
||||
$this->buffer .= \PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -41,6 +41,13 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
|
||||
{
|
||||
parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter);
|
||||
|
||||
if (null === $formatter) {
|
||||
// for BC reasons, stdErr has it own Formatter only when user don't inject a specific formatter.
|
||||
$this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$actualDecorated = $this->isDecorated();
|
||||
$this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $this->getFormatter());
|
||||
|
||||
@@ -131,7 +138,7 @@ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface
|
||||
$checks = [
|
||||
\function_exists('php_uname') ? php_uname('s') : '',
|
||||
getenv('OSTYPE'),
|
||||
PHP_OS,
|
||||
\PHP_OS,
|
||||
];
|
||||
|
||||
return false !== stripos(implode(';', $checks), 'OS400');
|
||||
|
||||
+5
-5
@@ -82,17 +82,17 @@ class ConsoleSectionOutput extends StreamOutput
|
||||
*/
|
||||
public function addContent(string $input)
|
||||
{
|
||||
foreach (explode(PHP_EOL, $input) as $lineContent) {
|
||||
foreach (explode(\PHP_EOL, $input) as $lineContent) {
|
||||
$this->lines += ceil($this->getDisplayLength($lineContent) / $this->terminal->getWidth()) ?: 1;
|
||||
$this->content[] = $lineContent;
|
||||
$this->content[] = PHP_EOL;
|
||||
$this->content[] = \PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doWrite($message, $newline)
|
||||
protected function doWrite(string $message, bool $newline)
|
||||
{
|
||||
if (!$this->isDecorated()) {
|
||||
parent::doWrite($message, $newline);
|
||||
@@ -136,8 +136,8 @@ class ConsoleSectionOutput extends StreamOutput
|
||||
return implode('', array_reverse($erasedContent));
|
||||
}
|
||||
|
||||
private function getDisplayLength(string $text): string
|
||||
private function getDisplayLength(string $text): int
|
||||
{
|
||||
return Helper::strlenWithoutDecoration($this->getFormatter(), str_replace("\t", ' ', $text));
|
||||
return Helper::width(Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text)));
|
||||
}
|
||||
}
|
||||
|
||||
+7
-2
@@ -11,7 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Console\Output;
|
||||
|
||||
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||
use Symfony\Component\Console\Formatter\NullOutputFormatter;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
|
||||
|
||||
/**
|
||||
@@ -24,6 +24,8 @@ use Symfony\Component\Console\Formatter\OutputFormatterInterface;
|
||||
*/
|
||||
class NullOutput implements OutputInterface
|
||||
{
|
||||
private $formatter;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -37,8 +39,11 @@ class NullOutput implements OutputInterface
|
||||
*/
|
||||
public function getFormatter()
|
||||
{
|
||||
if ($this->formatter) {
|
||||
return $this->formatter;
|
||||
}
|
||||
// to comply with the interface we must return a OutputFormatterInterface
|
||||
return new OutputFormatter();
|
||||
return $this->formatter = new NullOutputFormatter();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ abstract class Output implements OutputInterface
|
||||
public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, OutputFormatterInterface $formatter = null)
|
||||
{
|
||||
$this->verbosity = null === $verbosity ? self::VERBOSITY_NORMAL : $verbosity;
|
||||
$this->formatter = $formatter ?: new OutputFormatter();
|
||||
$this->formatter = $formatter ?? new OutputFormatter();
|
||||
$this->formatter->setDecorated($decorated);
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ abstract class Output implements OutputInterface
|
||||
break;
|
||||
}
|
||||
|
||||
$this->doWrite($message, $newline);
|
||||
$this->doWrite($message ?? '', $newline);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-8
@@ -20,15 +20,15 @@ use Symfony\Component\Console\Formatter\OutputFormatterInterface;
|
||||
*/
|
||||
interface OutputInterface
|
||||
{
|
||||
const VERBOSITY_QUIET = 16;
|
||||
const VERBOSITY_NORMAL = 32;
|
||||
const VERBOSITY_VERBOSE = 64;
|
||||
const VERBOSITY_VERY_VERBOSE = 128;
|
||||
const VERBOSITY_DEBUG = 256;
|
||||
public const VERBOSITY_QUIET = 16;
|
||||
public const VERBOSITY_NORMAL = 32;
|
||||
public const VERBOSITY_VERBOSE = 64;
|
||||
public const VERBOSITY_VERY_VERBOSE = 128;
|
||||
public const VERBOSITY_DEBUG = 256;
|
||||
|
||||
const OUTPUT_NORMAL = 1;
|
||||
const OUTPUT_RAW = 2;
|
||||
const OUTPUT_PLAIN = 4;
|
||||
public const OUTPUT_NORMAL = 1;
|
||||
public const OUTPUT_RAW = 2;
|
||||
public const OUTPUT_PLAIN = 4;
|
||||
|
||||
/**
|
||||
* Writes a message to the output.
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ class StreamOutput extends Output
|
||||
protected function doWrite(string $message, bool $newline)
|
||||
{
|
||||
if ($newline) {
|
||||
$message .= PHP_EOL;
|
||||
$message .= \PHP_EOL;
|
||||
}
|
||||
|
||||
@fwrite($this->stream, $message);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?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\Output;
|
||||
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
|
||||
|
||||
/**
|
||||
* A BufferedOutput that keeps only the last N chars.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class TrimmedBufferOutput extends Output
|
||||
{
|
||||
private $maxLength;
|
||||
private $buffer = '';
|
||||
|
||||
public function __construct(int $maxLength, ?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, OutputFormatterInterface $formatter = null) {
|
||||
if ($maxLength <= 0) {
|
||||
throw new InvalidArgumentException(sprintf('"%s()" expects a strictly positive maxLength. Got %d.', __METHOD__, $maxLength));
|
||||
}
|
||||
|
||||
parent::__construct($verbosity, $decorated, $formatter);
|
||||
$this->maxLength = $maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties buffer and returns its content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function fetch()
|
||||
{
|
||||
$content = $this->buffer;
|
||||
$this->buffer = '';
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doWrite(string $message, bool $newline)
|
||||
{
|
||||
$this->buffer .= $message;
|
||||
|
||||
if ($newline) {
|
||||
$this->buffer .= \PHP_EOL;
|
||||
}
|
||||
|
||||
$this->buffer = substr($this->buffer, 0 - $this->maxLength);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -169,7 +169,8 @@ class ChoiceQuestion extends Question
|
||||
throw new InvalidArgumentException(sprintf($errorMessage, $value));
|
||||
}
|
||||
|
||||
$multiselectChoices[] = (string) $result;
|
||||
// For associative choices, consistently return the key as string:
|
||||
$multiselectChoices[] = $isAssoc ? (string) $result : $result;
|
||||
}
|
||||
|
||||
if ($multiselect) {
|
||||
|
||||
+31
-11
@@ -30,10 +30,11 @@ class Question
|
||||
private $default;
|
||||
private $normalizer;
|
||||
private $trimmable = true;
|
||||
private $multiline = false;
|
||||
|
||||
/**
|
||||
* @param string $question The question to ask to the user
|
||||
* @param mixed $default The default answer to return if the user enters nothing
|
||||
* @param string $question The question to ask to the user
|
||||
* @param string|bool|int|float|null $default The default answer to return if the user enters nothing
|
||||
*/
|
||||
public function __construct(string $question, $default = null)
|
||||
{
|
||||
@@ -54,13 +55,33 @@ class Question
|
||||
/**
|
||||
* Returns the default answer.
|
||||
*
|
||||
* @return mixed
|
||||
* @return string|bool|int|float|null
|
||||
*/
|
||||
public function getDefault()
|
||||
{
|
||||
return $this->default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the user response accepts newline characters.
|
||||
*/
|
||||
public function isMultiline(): bool
|
||||
{
|
||||
return $this->multiline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the user response should accept newline characters.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setMultiline(bool $multiline): self
|
||||
{
|
||||
$this->multiline = $multiline;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the user response must be hidden.
|
||||
*
|
||||
@@ -74,13 +95,11 @@ class Question
|
||||
/**
|
||||
* Sets whether the user response must be hidden or not.
|
||||
*
|
||||
* @param bool $hidden
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws LogicException In case the autocompleter is also used
|
||||
*/
|
||||
public function setHidden($hidden)
|
||||
public function setHidden(bool $hidden)
|
||||
{
|
||||
if ($this->autocompleterCallback) {
|
||||
throw new LogicException('A hidden question cannot use the autocompleter.');
|
||||
@@ -104,11 +123,9 @@ class Question
|
||||
/**
|
||||
* Sets whether to fallback on non-hidden question if the response can not be hidden.
|
||||
*
|
||||
* @param bool $fallback
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setHiddenFallback($fallback)
|
||||
public function setHiddenFallback(bool $fallback)
|
||||
{
|
||||
$this->hiddenFallback = (bool) $fallback;
|
||||
|
||||
@@ -213,8 +230,11 @@ class Question
|
||||
*/
|
||||
public function setMaxAttempts(?int $attempts)
|
||||
{
|
||||
if (null !== $attempts && $attempts < 1) {
|
||||
throw new InvalidArgumentException('Maximum number of attempts must be a positive value.');
|
||||
if (null !== $attempts) {
|
||||
$attempts = (int) $attempts;
|
||||
if ($attempts < 1) {
|
||||
throw new InvalidArgumentException('Maximum number of attempts must be a positive value.');
|
||||
}
|
||||
}
|
||||
|
||||
$this->attempts = $attempts;
|
||||
|
||||
Vendored
+5
-5
@@ -7,11 +7,11 @@ interfaces.
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Documentation](https://symfony.com/doc/current/components/console.html)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
* [Documentation](https://symfony.com/doc/current/components/console.html)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
|
||||
Credits
|
||||
-------
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?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\SignalRegistry;
|
||||
|
||||
final class SignalRegistry
|
||||
{
|
||||
private $signalHandlers = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (\function_exists('pcntl_async_signals')) {
|
||||
pcntl_async_signals(true);
|
||||
}
|
||||
}
|
||||
|
||||
public function register(int $signal, callable $signalHandler): void
|
||||
{
|
||||
if (!isset($this->signalHandlers[$signal])) {
|
||||
$previousCallback = pcntl_signal_get_handler($signal);
|
||||
|
||||
if (\is_callable($previousCallback)) {
|
||||
$this->signalHandlers[$signal][] = $previousCallback;
|
||||
}
|
||||
}
|
||||
|
||||
$this->signalHandlers[$signal][] = $signalHandler;
|
||||
|
||||
pcntl_signal($signal, [$this, 'handle']);
|
||||
}
|
||||
|
||||
public static function isSupported(): bool
|
||||
{
|
||||
if (!\function_exists('pcntl_signal')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (\in_array('pcntl_signal', explode(',', ini_get('disable_functions')))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function handle(int $signal): void
|
||||
{
|
||||
$count = \count($this->signalHandlers[$signal]);
|
||||
|
||||
foreach ($this->signalHandlers[$signal] as $i => $signalHandler) {
|
||||
$hasNext = $i !== $count - 1;
|
||||
$signalHandler($signal, $hasNext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
*/
|
||||
class SingleCommandApplication extends Command
|
||||
{
|
||||
private $version = 'UNKNOWN';
|
||||
private $autoExit = true;
|
||||
private $running = false;
|
||||
|
||||
public function setVersion(string $version): self
|
||||
{
|
||||
$this->version = $version;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
public function setAutoExit(bool $autoExit): self
|
||||
{
|
||||
$this->autoExit = $autoExit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function run(InputInterface $input = null, OutputInterface $output = null): int
|
||||
{
|
||||
if ($this->running) {
|
||||
return parent::run($input, $output);
|
||||
}
|
||||
|
||||
// We use the command name as the application name
|
||||
$application = new Application($this->getName() ?: 'UNKNOWN', $this->version);
|
||||
$application->setAutoExit($this->autoExit);
|
||||
// Fix the usage of the command displayed with "--help"
|
||||
$this->setName($_SERVER['argv'][0]);
|
||||
$application->add($this);
|
||||
$application->setDefaultCommand($this->getName(), true);
|
||||
|
||||
$this->running = true;
|
||||
try {
|
||||
$ret = $application->run($input, $output);
|
||||
} finally {
|
||||
$this->running = false;
|
||||
}
|
||||
|
||||
return $ret ?? 1;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -35,7 +35,7 @@ abstract class OutputStyle implements OutputInterface, StyleInterface
|
||||
*/
|
||||
public function newLine(int $count = 1)
|
||||
{
|
||||
$this->output->write(str_repeat(PHP_EOL, $count));
|
||||
$this->output->write(str_repeat(\PHP_EOL, $count));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ interface StyleInterface
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function ask(string $question, ?string $default = null, callable $validator = null);
|
||||
public function ask(string $question, string $default = null, callable $validator = null);
|
||||
|
||||
/**
|
||||
* Asks a question with the user input hidden.
|
||||
|
||||
+32
-18
@@ -21,8 +21,8 @@ use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Helper\TableCell;
|
||||
use Symfony\Component\Console\Helper\TableSeparator;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\BufferedOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Output\TrimmedBufferOutput;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
@@ -35,7 +35,7 @@ use Symfony\Component\Console\Terminal;
|
||||
*/
|
||||
class SymfonyStyle extends OutputStyle
|
||||
{
|
||||
const MAX_LINE_LENGTH = 120;
|
||||
public const MAX_LINE_LENGTH = 120;
|
||||
|
||||
private $input;
|
||||
private $questionHelper;
|
||||
@@ -46,7 +46,7 @@ class SymfonyStyle extends OutputStyle
|
||||
public function __construct(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter());
|
||||
$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);
|
||||
@@ -59,7 +59,7 @@ class SymfonyStyle extends OutputStyle
|
||||
*
|
||||
* @param string|array $messages The message to write in the block
|
||||
*/
|
||||
public function block($messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true)
|
||||
public function block($messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true)
|
||||
{
|
||||
$messages = \is_array($messages) ? array_values($messages) : [$messages];
|
||||
|
||||
@@ -76,7 +76,7 @@ class SymfonyStyle extends OutputStyle
|
||||
$this->autoPrependBlock();
|
||||
$this->writeln([
|
||||
sprintf('<comment>%s</>', OutputFormatter::escapeTrailingBackslash($message)),
|
||||
sprintf('<comment>%s</>', str_repeat('=', Helper::strlenWithoutDecoration($this->getFormatter(), $message))),
|
||||
sprintf('<comment>%s</>', str_repeat('=', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
|
||||
]);
|
||||
$this->newLine();
|
||||
}
|
||||
@@ -89,7 +89,7 @@ class SymfonyStyle extends OutputStyle
|
||||
$this->autoPrependBlock();
|
||||
$this->writeln([
|
||||
sprintf('<comment>%s</>', OutputFormatter::escapeTrailingBackslash($message)),
|
||||
sprintf('<comment>%s</>', str_repeat('-', Helper::strlenWithoutDecoration($this->getFormatter(), $message))),
|
||||
sprintf('<comment>%s</>', str_repeat('-', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))),
|
||||
]);
|
||||
$this->newLine();
|
||||
}
|
||||
@@ -163,6 +163,16 @@ class SymfonyStyle extends OutputStyle
|
||||
$this->block($message, 'NOTE', 'fg=yellow', ' ! ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an info message.
|
||||
*
|
||||
* @param string|array $message
|
||||
*/
|
||||
public function info($message)
|
||||
{
|
||||
$this->block($message, 'INFO', 'fg=green', ' ', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
@@ -254,7 +264,7 @@ class SymfonyStyle extends OutputStyle
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function ask(string $question, ?string $default = null, $validator = null)
|
||||
public function ask(string $question, string $default = null, callable $validator = null)
|
||||
{
|
||||
$question = new Question($question, $default);
|
||||
$question->setValidator($validator);
|
||||
@@ -265,7 +275,7 @@ class SymfonyStyle extends OutputStyle
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function askHidden(string $question, $validator = null)
|
||||
public function askHidden(string $question, callable $validator = null)
|
||||
{
|
||||
$question = new Question($question);
|
||||
|
||||
@@ -278,7 +288,7 @@ class SymfonyStyle extends OutputStyle
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function confirm($question, $default = true)
|
||||
public function confirm(string $question, bool $default = true)
|
||||
{
|
||||
return $this->askQuestion(new ConfirmationQuestion($question, $default));
|
||||
}
|
||||
@@ -290,7 +300,7 @@ class SymfonyStyle extends OutputStyle
|
||||
{
|
||||
if (null !== $default) {
|
||||
$values = array_flip($choices);
|
||||
$default = isset($values[$default]) ? $values[$default] : $default;
|
||||
$default = $values[$default] ?? $default;
|
||||
}
|
||||
|
||||
return $this->askQuestion(new ChoiceQuestion($question, $choices, $default));
|
||||
@@ -422,7 +432,7 @@ class SymfonyStyle extends OutputStyle
|
||||
|
||||
private function autoPrependBlock(): void
|
||||
{
|
||||
$chars = substr(str_replace(PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2);
|
||||
$chars = substr(str_replace(\PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2);
|
||||
|
||||
if (!isset($chars[0])) {
|
||||
$this->newLine(); //empty history, so we should start with a new line.
|
||||
@@ -437,22 +447,21 @@ class SymfonyStyle extends OutputStyle
|
||||
{
|
||||
$fetched = $this->bufferedOutput->fetch();
|
||||
//Prepend new line if last char isn't EOL:
|
||||
if ("\n" !== substr($fetched, -1)) {
|
||||
if (!str_ends_with($fetched, "\n")) {
|
||||
$this->newLine();
|
||||
}
|
||||
}
|
||||
|
||||
private function writeBuffer(string $message, bool $newLine, int $type): void
|
||||
{
|
||||
// We need to know if the two last chars are PHP_EOL
|
||||
// Preserve the last 4 chars inserted (PHP_EOL on windows is two chars) in the history buffer
|
||||
$this->bufferedOutput->write(substr($message, -4), $newLine, $type);
|
||||
// We need to know if the last chars are PHP_EOL
|
||||
$this->bufferedOutput->write($message, $newLine, $type);
|
||||
}
|
||||
|
||||
private function createBlock(iterable $messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array
|
||||
{
|
||||
$indentLength = 0;
|
||||
$prefixLength = Helper::strlenWithoutDecoration($this->getFormatter(), $prefix);
|
||||
$prefixLength = Helper::width(Helper::removeDecoration($this->getFormatter(), $prefix));
|
||||
$lines = [];
|
||||
|
||||
if (null !== $type) {
|
||||
@@ -467,7 +476,12 @@ class SymfonyStyle extends OutputStyle
|
||||
$message = OutputFormatter::escape($message);
|
||||
}
|
||||
|
||||
$lines = array_merge($lines, explode(PHP_EOL, wordwrap($message, $this->lineLength - $prefixLength - $indentLength, PHP_EOL, true)));
|
||||
$decorationLength = Helper::width($message) - Helper::width(Helper::removeDecoration($this->getFormatter(), $message));
|
||||
$messageLineLength = min($this->lineLength - $prefixLength - $indentLength + $decorationLength, $this->lineLength);
|
||||
$messageLines = explode(\PHP_EOL, wordwrap($message, $messageLineLength, \PHP_EOL, true));
|
||||
foreach ($messageLines as $messageLine) {
|
||||
$lines[] = $messageLine;
|
||||
}
|
||||
|
||||
if (\count($messages) > 1 && $key < \count($messages) - 1) {
|
||||
$lines[] = '';
|
||||
@@ -487,7 +501,7 @@ class SymfonyStyle extends OutputStyle
|
||||
}
|
||||
|
||||
$line = $prefix.$line;
|
||||
$line .= str_repeat(' ', $this->lineLength - Helper::strlenWithoutDecoration($this->getFormatter(), $line));
|
||||
$line .= str_repeat(' ', max($this->lineLength - Helper::width(Helper::removeDecoration($this->getFormatter(), $line)), 0));
|
||||
|
||||
if ($style) {
|
||||
$line = sprintf('<%s>%s</>', $style, $line);
|
||||
|
||||
Vendored
+5
@@ -66,6 +66,11 @@ class Terminal
|
||||
return self::$stty;
|
||||
}
|
||||
|
||||
// skip check if exec function is disabled
|
||||
if (!\function_exists('exec')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
exec('stty 2>&1', $output, $exitcode);
|
||||
|
||||
return self::$stty = 0 === $exitcode;
|
||||
|
||||
+13
-5
@@ -29,6 +29,8 @@ trait TesterTrait
|
||||
/**
|
||||
* Gets the display returned by the last execution of the command or application.
|
||||
*
|
||||
* @throws \RuntimeException If it's called before the execute method
|
||||
*
|
||||
* @return string The display
|
||||
*/
|
||||
public function getDisplay(bool $normalize = false)
|
||||
@@ -42,7 +44,7 @@ trait TesterTrait
|
||||
$display = stream_get_contents($this->output->getStream());
|
||||
|
||||
if ($normalize) {
|
||||
$display = str_replace(PHP_EOL, "\n", $display);
|
||||
$display = str_replace(\PHP_EOL, "\n", $display);
|
||||
}
|
||||
|
||||
return $display;
|
||||
@@ -66,7 +68,7 @@ trait TesterTrait
|
||||
$display = stream_get_contents($this->output->getErrorOutput()->getStream());
|
||||
|
||||
if ($normalize) {
|
||||
$display = str_replace(PHP_EOL, "\n", $display);
|
||||
$display = str_replace(\PHP_EOL, "\n", $display);
|
||||
}
|
||||
|
||||
return $display;
|
||||
@@ -95,10 +97,16 @@ trait TesterTrait
|
||||
/**
|
||||
* Gets the status code returned by the last execution of the command or application.
|
||||
*
|
||||
* @throws \RuntimeException If it's called before the execute method
|
||||
*
|
||||
* @return int The status code
|
||||
*/
|
||||
public function getStatusCode()
|
||||
{
|
||||
if (null === $this->statusCode) {
|
||||
throw new \RuntimeException('Status code not initialized, did you execute the command before requesting the status code?');
|
||||
}
|
||||
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
@@ -139,8 +147,8 @@ trait TesterTrait
|
||||
}
|
||||
} else {
|
||||
$this->output = new ConsoleOutput(
|
||||
isset($options['verbosity']) ? $options['verbosity'] : ConsoleOutput::VERBOSITY_NORMAL,
|
||||
isset($options['decorated']) ? $options['decorated'] : null
|
||||
$options['verbosity'] ?? ConsoleOutput::VERBOSITY_NORMAL,
|
||||
$options['decorated'] ?? null
|
||||
);
|
||||
|
||||
$errorOutput = new StreamOutput(fopen('php://memory', 'w', false));
|
||||
@@ -168,7 +176,7 @@ trait TesterTrait
|
||||
$stream = fopen('php://memory', 'r+', false);
|
||||
|
||||
foreach ($inputs as $input) {
|
||||
fwrite($stream, $input.PHP_EOL);
|
||||
fwrite($stream, $input.\PHP_EOL);
|
||||
}
|
||||
|
||||
rewind($stream);
|
||||
|
||||
Vendored
+12
-12
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"type": "library",
|
||||
"description": "Symfony Console Component",
|
||||
"keywords": [],
|
||||
"description": "Eases the creation of beautiful and testable command line interfaces",
|
||||
"keywords": ["console", "cli", "command line", "terminal"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
@@ -16,10 +16,13 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.2.5",
|
||||
"php": ">=7.2.5",
|
||||
"symfony/deprecation-contracts": "^2.1",
|
||||
"symfony/polyfill-mbstring": "~1.0",
|
||||
"symfony/polyfill-php73": "^1.8",
|
||||
"symfony/service-contracts": "^1.1|^2"
|
||||
"symfony/polyfill-php80": "^1.16",
|
||||
"symfony/service-contracts": "^1.1|^2",
|
||||
"symfony/string": "^5.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/config": "^4.4|^5.0",
|
||||
@@ -28,10 +31,10 @@
|
||||
"symfony/lock": "^4.4|^5.0",
|
||||
"symfony/process": "^4.4|^5.0",
|
||||
"symfony/var-dumper": "^4.4|^5.0",
|
||||
"psr/log": "~1.0"
|
||||
"psr/log": "^1|^2"
|
||||
},
|
||||
"provide": {
|
||||
"psr/log-implementation": "1.0"
|
||||
"psr/log-implementation": "1.0|2.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/event-dispatcher": "",
|
||||
@@ -40,7 +43,9 @@
|
||||
"psr/log": "For using the console logger"
|
||||
},
|
||||
"conflict": {
|
||||
"psr/log": ">=3",
|
||||
"symfony/dependency-injection": "<4.4",
|
||||
"symfony/dotenv": "<5.1",
|
||||
"symfony/event-dispatcher": "<4.4",
|
||||
"symfony/lock": "<4.4",
|
||||
"symfony/process": "<4.4"
|
||||
@@ -51,10 +56,5 @@
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "5.0-dev"
|
||||
}
|
||||
}
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
vendor/
|
||||
composer.lock
|
||||
phpunit.xml
|
||||
@@ -0,0 +1,5 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
The changelog is maintained for all Symfony contracts at the following URL:
|
||||
https://github.com/symfony/contracts/blob/main/CHANGELOG.md
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2020-2021 Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
Symfony Deprecation Contracts
|
||||
=============================
|
||||
|
||||
A generic function and convention to trigger deprecation notices.
|
||||
|
||||
This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
|
||||
|
||||
By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
|
||||
the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
|
||||
|
||||
The function requires at least 3 arguments:
|
||||
- the name of the Composer package that is triggering the deprecation
|
||||
- the version of the package that introduced the deprecation
|
||||
- the message of the deprecation
|
||||
- more arguments can be provided: they will be inserted in the message using `printf()` formatting
|
||||
|
||||
Example:
|
||||
```php
|
||||
trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
|
||||
```
|
||||
|
||||
This will generate the following message:
|
||||
`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
|
||||
|
||||
While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty
|
||||
`function trigger_deprecation() {}` in your application.
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
"type": "library",
|
||||
"description": "A generic function and convention to trigger deprecation notices",
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"function.php"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "2.4-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/contracts",
|
||||
"url": "https://github.com/symfony/contracts"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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.
|
||||
*/
|
||||
|
||||
if (!function_exists('trigger_deprecation')) {
|
||||
/**
|
||||
* Triggers a silenced deprecation notice.
|
||||
*
|
||||
* @param string $package The name of the Composer package that is triggering the deprecation
|
||||
* @param string $version The version of the package that introduced the deprecation
|
||||
* @param string $message The message of the deprecation
|
||||
* @param mixed ...$args Values to insert in the message using printf() formatting
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
function trigger_deprecation(string $package, string $version, string $message, ...$args): void
|
||||
{
|
||||
@trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
/Tests export-ignore
|
||||
/phpunit.xml.dist export-ignore
|
||||
/.gitignore export-ignore
|
||||
@@ -11,9 +11,13 @@
|
||||
|
||||
namespace Symfony\Component\Lock\Exception;
|
||||
|
||||
use Symfony\Component\Lock\Lock;
|
||||
|
||||
/**
|
||||
* LockConflictedException is thrown when a lock is acquired by someone else.
|
||||
*
|
||||
* In non-blocking mode it is caught by {@see Lock::acquire()} and {@see Lock::acquireRead()}.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class LockConflictedException extends \RuntimeException implements ExceptionInterface
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2016-2020 Fabien Potencier
|
||||
Copyright (c) 2016-2021 Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
Vendored
+14
@@ -50,6 +50,19 @@ final class Lock implements LockInterface, LoggerAwareInterface
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
|
||||
}
|
||||
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically releases the underlying lock when the object is destructed.
|
||||
*/
|
||||
@@ -67,6 +80,7 @@ final class Lock implements LockInterface, LoggerAwareInterface
|
||||
*/
|
||||
public function acquire($blocking = false): bool
|
||||
{
|
||||
$this->key->resetLifetime();
|
||||
try {
|
||||
if ($blocking) {
|
||||
if (!$this->store instanceof StoreInterface && !$this->store instanceof BlockingStoreInterface) {
|
||||
|
||||
Vendored
+8
-5
@@ -1,11 +1,14 @@
|
||||
Lock Component
|
||||
==============
|
||||
|
||||
The Lock component creates and manages locks, a mechanism to provide exclusive
|
||||
access to a shared resource.
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Documentation](https://symfony.com/doc/master/components/lock.html)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
* [Documentation](https://symfony.com/doc/master/components/lock.html)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
||||
|
||||
+11
-6
@@ -29,8 +29,8 @@ use Symfony\Component\Lock\Strategy\StrategyInterface;
|
||||
*/
|
||||
class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
use ExpiringStoreTrait;
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/** @var PersistingStoreInterface[] */
|
||||
private $stores;
|
||||
@@ -99,8 +99,8 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', \get_class($this)));
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), \E_USER_DEPRECATED);
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', static::class));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,9 +171,14 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
$storesCount = \count($this->stores);
|
||||
|
||||
foreach ($this->stores as $store) {
|
||||
if ($store->exists($key)) {
|
||||
++$successCount;
|
||||
} else {
|
||||
try {
|
||||
if ($store->exists($key)) {
|
||||
++$successCount;
|
||||
} else {
|
||||
++$failureCount;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->debug('One store failed to check the "{resource}" lock.', ['resource' => $key, 'store' => $store, 'exception' => $e]);
|
||||
++$failureCount;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -74,7 +74,7 @@ class FlockStore implements StoreInterface, BlockingStoreInterface
|
||||
|
||||
$fileName = sprintf('%s/sf.%s.%s.lock',
|
||||
$this->lockPath,
|
||||
preg_replace('/[^a-z0-9\._-]+/i', '-', $key),
|
||||
substr(preg_replace('/[^a-z0-9\._-]+/i', '-', $key), 0, 50),
|
||||
strtr(substr(base64_encode(hash('sha256', $key, true)), 0, 7), '/', '_')
|
||||
);
|
||||
|
||||
@@ -96,7 +96,7 @@ class FlockStore implements StoreInterface, BlockingStoreInterface
|
||||
|
||||
// On Windows, even if PHP doc says the contrary, LOCK_NB works, see
|
||||
// https://bugs.php.net/54129
|
||||
if (!flock($handle, LOCK_EX | ($blocking ? 0 : LOCK_NB))) {
|
||||
if (!flock($handle, \LOCK_EX | ($blocking ? 0 : \LOCK_NB))) {
|
||||
fclose($handle);
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
@@ -124,7 +124,7 @@ class FlockStore implements StoreInterface, BlockingStoreInterface
|
||||
|
||||
$handle = $key->getState(__CLASS__);
|
||||
|
||||
flock($handle, LOCK_UN | LOCK_NB);
|
||||
flock($handle, \LOCK_UN | \LOCK_NB);
|
||||
fclose($handle);
|
||||
|
||||
$key->removeState(__CLASS__);
|
||||
|
||||
+7
-7
@@ -43,11 +43,11 @@ class MemcachedStore implements StoreInterface
|
||||
public function __construct(\Memcached $memcached, int $initialTtl = 300)
|
||||
{
|
||||
if (!static::isSupported()) {
|
||||
throw new InvalidArgumentException('Memcached extension is required');
|
||||
throw new InvalidArgumentException('Memcached extension is required.');
|
||||
}
|
||||
|
||||
if ($initialTtl < 1) {
|
||||
throw new InvalidArgumentException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
|
||||
throw new InvalidArgumentException(sprintf('"%s()" expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
|
||||
}
|
||||
|
||||
$this->memcached = $memcached;
|
||||
@@ -76,8 +76,8 @@ class MemcachedStore implements StoreInterface
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', \get_class($this)));
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), \E_USER_DEPRECATED);
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', static::class));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,7 +86,7 @@ class MemcachedStore implements StoreInterface
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
if ($ttl < 1) {
|
||||
throw new InvalidTtlException(sprintf('%s() expects a TTL greater or equals to 1 second. Got %s.', __METHOD__, $ttl));
|
||||
throw new InvalidTtlException(sprintf('"%s()" expects a TTL greater or equals to 1 second. Got %s.', __METHOD__, $ttl));
|
||||
}
|
||||
|
||||
// Interface defines a float value but Store required an integer.
|
||||
@@ -94,7 +94,7 @@ class MemcachedStore implements StoreInterface
|
||||
|
||||
$token = $this->getUniqueToken($key);
|
||||
|
||||
list($value, $cas) = $this->getValueAndCas($key);
|
||||
[$value, $cas] = $this->getValueAndCas($key);
|
||||
|
||||
$key->reduceLifetime($ttl);
|
||||
// Could happens when we ask a putOff after a timeout but in luck nobody steal the lock
|
||||
@@ -126,7 +126,7 @@ class MemcachedStore implements StoreInterface
|
||||
{
|
||||
$token = $this->getUniqueToken($key);
|
||||
|
||||
list($value, $cas) = $this->getValueAndCas($key);
|
||||
[$value, $cas] = $this->getValueAndCas($key);
|
||||
|
||||
if ($value !== $token) {
|
||||
// we are not the owner of the lock. Nothing to do.
|
||||
|
||||
+48
-23
@@ -14,6 +14,7 @@ namespace Symfony\Component\Lock\Store;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\InvalidTtlException;
|
||||
@@ -82,12 +83,12 @@ class PdoStore implements StoreInterface
|
||||
throw new InvalidArgumentException(sprintf('"%s" requires gcProbability between 0 and 1, "%f" given.', __METHOD__, $gcProbability));
|
||||
}
|
||||
if ($initialTtl < 1) {
|
||||
throw new InvalidTtlException(sprintf('%s() expects a strictly positive TTL, "%d" given.', __METHOD__, $initialTtl));
|
||||
throw new InvalidTtlException(sprintf('"%s()" expects a strictly positive TTL, "%d" given.', __METHOD__, $initialTtl));
|
||||
}
|
||||
|
||||
if ($connOrDsn instanceof \PDO) {
|
||||
if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
|
||||
throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __METHOD__));
|
||||
throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __METHOD__));
|
||||
}
|
||||
|
||||
$this->conn = $connOrDsn;
|
||||
@@ -126,7 +127,7 @@ class PdoStore implements StoreInterface
|
||||
|
||||
try {
|
||||
$stmt->execute();
|
||||
} catch (DBALException $e) {
|
||||
} catch (DBALException | Exception $e) {
|
||||
// the lock is already acquired. It could be us. Let's try to put off.
|
||||
$this->putOffExpiration($key, $this->initialTtl);
|
||||
} catch (\PDOException $e) {
|
||||
@@ -134,7 +135,7 @@ class PdoStore implements StoreInterface
|
||||
$this->putOffExpiration($key, $this->initialTtl);
|
||||
}
|
||||
|
||||
if ($this->gcProbability > 0 && (1.0 === $this->gcProbability || (random_int(0, PHP_INT_MAX) / PHP_INT_MAX) <= $this->gcProbability)) {
|
||||
if ($this->gcProbability > 0 && (1.0 === $this->gcProbability || (random_int(0, \PHP_INT_MAX) / \PHP_INT_MAX) <= $this->gcProbability)) {
|
||||
$this->prune();
|
||||
}
|
||||
|
||||
@@ -146,7 +147,7 @@ class PdoStore implements StoreInterface
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), \E_USER_DEPRECATED);
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not supports blocking locks.', __METHOD__));
|
||||
}
|
||||
|
||||
@@ -156,7 +157,7 @@ class PdoStore implements StoreInterface
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
if ($ttl < 1) {
|
||||
throw new InvalidTtlException(sprintf('%s() expects a TTL greater or equals to 1 second. Got %s.', __METHOD__, $ttl));
|
||||
throw new InvalidTtlException(sprintf('"%s()" expects a TTL greater or equals to 1 second. Got %s.', __METHOD__, $ttl));
|
||||
}
|
||||
|
||||
$key->reduceLifetime($ttl);
|
||||
@@ -168,10 +169,10 @@ class PdoStore implements StoreInterface
|
||||
$stmt->bindValue(':id', $this->getHashedKey($key));
|
||||
$stmt->bindValue(':token1', $uniqueToken);
|
||||
$stmt->bindValue(':token2', $uniqueToken);
|
||||
$stmt->execute();
|
||||
$result = $stmt->execute();
|
||||
|
||||
// If this method is called twice in the same second, the row wouldn't be updated. We have to call exists to know if we are the owner
|
||||
if (!$stmt->rowCount() && !$this->exists($key)) {
|
||||
if (!(\is_object($result) ? $result : $stmt)->rowCount() && !$this->exists($key)) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
@@ -201,9 +202,9 @@ class PdoStore implements StoreInterface
|
||||
|
||||
$stmt->bindValue(':id', $this->getHashedKey($key));
|
||||
$stmt->bindValue(':token', $this->getUniqueToken($key));
|
||||
$stmt->execute();
|
||||
$result = $stmt->execute();
|
||||
|
||||
return (bool) $stmt->fetchColumn();
|
||||
return (bool) (\is_object($result) ? $result->fetchOne() : $stmt->fetchColumn());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -249,6 +250,7 @@ class PdoStore implements StoreInterface
|
||||
*
|
||||
* @throws \PDOException When the table already exists
|
||||
* @throws DBALException When the table already exists
|
||||
* @throws Exception When the table already exists
|
||||
* @throws \DomainException When an unsupported PDO driver is used
|
||||
*/
|
||||
public function createTable(): void
|
||||
@@ -266,7 +268,11 @@ class PdoStore implements StoreInterface
|
||||
$table->setPrimaryKey([$this->idCol]);
|
||||
|
||||
foreach ($schema->toSql($conn->getDatabasePlatform()) as $sql) {
|
||||
$conn->exec($sql);
|
||||
if (method_exists($conn, 'executeStatement')) {
|
||||
$conn->executeStatement($sql);
|
||||
} else {
|
||||
$conn->exec($sql);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -292,7 +298,11 @@ class PdoStore implements StoreInterface
|
||||
throw new \DomainException(sprintf('Creating the lock table is currently not implemented for PDO driver "%s".', $driver));
|
||||
}
|
||||
|
||||
$conn->exec($sql);
|
||||
if (method_exists($conn, 'executeStatement')) {
|
||||
$conn->executeStatement($sql);
|
||||
} else {
|
||||
$conn->exec($sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,7 +312,12 @@ class PdoStore implements StoreInterface
|
||||
{
|
||||
$sql = "DELETE FROM $this->table WHERE $this->expirationCol <= {$this->getCurrentTimestampStatement()}";
|
||||
|
||||
$this->getConnection()->exec($sql);
|
||||
$conn = $this->getConnection();
|
||||
if (method_exists($conn, 'executeStatement')) {
|
||||
$conn->executeStatement($sql);
|
||||
} else {
|
||||
$conn->exec($sql);
|
||||
}
|
||||
}
|
||||
|
||||
private function getDriver(): string
|
||||
@@ -315,25 +330,35 @@ class PdoStore implements StoreInterface
|
||||
if ($con instanceof \PDO) {
|
||||
$this->driver = $con->getAttribute(\PDO::ATTR_DRIVER_NAME);
|
||||
} else {
|
||||
switch ($this->driver = $con->getDriver()->getName()) {
|
||||
case 'mysqli':
|
||||
case 'pdo_mysql':
|
||||
case 'drizzle_pdo_mysql':
|
||||
$driver = $con->getDriver();
|
||||
|
||||
switch (true) {
|
||||
case $driver instanceof \Doctrine\DBAL\Driver\Mysqli\Driver:
|
||||
throw new \LogicException(sprintf('The adapter "%s" does not support the mysqli driver, use pdo_mysql instead.', static::class));
|
||||
case $driver instanceof \Doctrine\DBAL\Driver\AbstractMySQLDriver:
|
||||
$this->driver = 'mysql';
|
||||
break;
|
||||
case 'pdo_sqlite':
|
||||
case $driver instanceof \Doctrine\DBAL\Driver\PDOSqlite\Driver:
|
||||
case $driver instanceof \Doctrine\DBAL\Driver\PDO\SQLite\Driver:
|
||||
$this->driver = 'sqlite';
|
||||
break;
|
||||
case 'pdo_pgsql':
|
||||
case $driver instanceof \Doctrine\DBAL\Driver\PDOPgSql\Driver:
|
||||
case $driver instanceof \Doctrine\DBAL\Driver\PDO\PgSQL\Driver:
|
||||
$this->driver = 'pgsql';
|
||||
break;
|
||||
case 'oci8':
|
||||
case 'pdo_oracle':
|
||||
case $driver instanceof \Doctrine\DBAL\Driver\OCI8\Driver:
|
||||
case $driver instanceof \Doctrine\DBAL\Driver\PDOOracle\Driver:
|
||||
case $driver instanceof \Doctrine\DBAL\Driver\PDO\OCI\Driver:
|
||||
$this->driver = 'oci';
|
||||
break;
|
||||
case 'pdo_sqlsrv':
|
||||
case $driver instanceof \Doctrine\DBAL\Driver\SQLSrv\Driver:
|
||||
case $driver instanceof \Doctrine\DBAL\Driver\PDOSqlsrv\Driver:
|
||||
case $driver instanceof \Doctrine\DBAL\Driver\PDO\SQLSrv\Driver:
|
||||
$this->driver = 'sqlsrv';
|
||||
break;
|
||||
default:
|
||||
$this->driver = \get_class($driver);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +366,7 @@ class PdoStore implements StoreInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a SQL function to get the current timestamp regarding the current connection's driver.
|
||||
* Provides an SQL function to get the current timestamp regarding the current connection's driver.
|
||||
*/
|
||||
private function getCurrentTimestampStatement(): string
|
||||
{
|
||||
|
||||
+10
-10
@@ -33,20 +33,20 @@ class RedisStore implements StoreInterface
|
||||
private $initialTtl;
|
||||
|
||||
/**
|
||||
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface $redisClient
|
||||
* @param float $initialTtl the expiration delay of locks in seconds
|
||||
* @param \Redis|\RedisArray|\RedisCluster|RedisProxy|RedisClusterProxy|\Predis\ClientInterface $redis
|
||||
* @param float $initialTtl The expiration delay of locks in seconds
|
||||
*/
|
||||
public function __construct($redisClient, float $initialTtl = 300.0)
|
||||
public function __construct($redis, float $initialTtl = 300.0)
|
||||
{
|
||||
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\ClientInterface && !$redisClient instanceof RedisProxy) {
|
||||
throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\ClientInterface, %s given', __METHOD__, \is_object($redisClient) ? \get_class($redisClient) : \gettype($redisClient)));
|
||||
if (!$redis instanceof \Redis && !$redis instanceof \RedisArray && !$redis instanceof \RedisCluster && !$redis instanceof \Predis\ClientInterface && !$redis instanceof RedisProxy && !$redis instanceof RedisClusterProxy) {
|
||||
throw new InvalidArgumentException(sprintf('"%s()" expects parameter 1 to be Redis, RedisArray, RedisCluster, RedisProxy, RedisClusterProxy or Predis\ClientInterface, "%s" given.', __METHOD__, \is_object($redis) ? \get_class($redis) : \gettype($redis)));
|
||||
}
|
||||
|
||||
if ($initialTtl <= 0) {
|
||||
throw new InvalidTtlException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
|
||||
throw new InvalidTtlException(sprintf('"%s()" expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
|
||||
}
|
||||
|
||||
$this->redis = $redisClient;
|
||||
$this->redis = $redis;
|
||||
$this->initialTtl = $initialTtl;
|
||||
}
|
||||
|
||||
@@ -80,8 +80,8 @@ class RedisStore implements StoreInterface
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', \get_class($this)));
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), \E_USER_DEPRECATED);
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not support blocking locks.', static::class));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,7 +153,7 @@ class RedisStore implements StoreInterface
|
||||
return $this->redis->eval(...array_merge([$script, 1, $resource], $args));
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('%s() expects being initialized with a Redis, RedisArray, RedisCluster or Predis\ClientInterface, %s given', __METHOD__, \is_object($this->redis) ? \get_class($this->redis) : \gettype($this->redis)));
|
||||
throw new InvalidArgumentException(sprintf('"%s()" expects being initialized with a Redis, RedisArray, RedisCluster or Predis\ClientInterface, "%s" given.', __METHOD__, \is_object($this->redis) ? \get_class($this->redis) : \gettype($this->redis)));
|
||||
}
|
||||
|
||||
private function getUniqueToken(Key $key): string
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ class RetryTillSaveStore implements BlockingStoreInterface, StoreInterface, Logg
|
||||
* @param int $retrySleep Duration in ms between 2 retry
|
||||
* @param int $retryCount Maximum amount of retry
|
||||
*/
|
||||
public function __construct(PersistingStoreInterface $decorated, int $retrySleep = 100, int $retryCount = PHP_INT_MAX)
|
||||
public function __construct(PersistingStoreInterface $decorated, int $retrySleep = 100, int $retryCount = \PHP_INT_MAX)
|
||||
{
|
||||
$this->decorated = $decorated;
|
||||
$this->retrySleep = $retrySleep;
|
||||
|
||||
+2
-2
@@ -37,7 +37,7 @@ class SemaphoreStore implements StoreInterface, BlockingStoreInterface
|
||||
public function __construct()
|
||||
{
|
||||
if (!static::isSupported()) {
|
||||
throw new InvalidArgumentException('Semaphore extension (sysvsem) is required');
|
||||
throw new InvalidArgumentException('Semaphore extension (sysvsem) is required.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class SemaphoreStore implements StoreInterface, BlockingStoreInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$keyId = crc32($key);
|
||||
$keyId = unpack('i', md5($key, true))[1];
|
||||
$resource = sem_get($keyId);
|
||||
$acquired = @sem_acquire($resource, !$blocking);
|
||||
|
||||
|
||||
+21
-21
@@ -33,7 +33,7 @@ class StoreFactory
|
||||
public static function createStore($connection)
|
||||
{
|
||||
if (!\is_string($connection) && !\is_object($connection)) {
|
||||
throw new \TypeError(sprintf('Argument 1 passed to %s() must be a string or a connection object, %s given.', __METHOD__, \gettype($connection)));
|
||||
throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a string or a connection object, "%s" given.', __METHOD__, \gettype($connection)));
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
@@ -56,45 +56,45 @@ class StoreFactory
|
||||
return new ZookeeperStore($connection);
|
||||
|
||||
case !\is_string($connection):
|
||||
throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', \get_class($connection)));
|
||||
throw new InvalidArgumentException(sprintf('Unsupported Connection: "%s".', \get_class($connection)));
|
||||
case 'flock' === $connection:
|
||||
return new FlockStore();
|
||||
|
||||
case 0 === strpos($connection, 'flock://'):
|
||||
case str_starts_with($connection, 'flock://'):
|
||||
return new FlockStore(substr($connection, 8));
|
||||
|
||||
case 'semaphore' === $connection:
|
||||
return new SemaphoreStore();
|
||||
|
||||
case 0 === strpos($connection, 'redis://'):
|
||||
case 0 === strpos($connection, 'rediss://'):
|
||||
case 0 === strpos($connection, 'memcached://'):
|
||||
case str_starts_with($connection, 'redis:'):
|
||||
case str_starts_with($connection, 'rediss:'):
|
||||
case str_starts_with($connection, 'memcached:'):
|
||||
if (!class_exists(AbstractAdapter::class)) {
|
||||
throw new InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection));
|
||||
}
|
||||
$storeClass = 0 === strpos($connection, 'memcached://') ? MemcachedStore::class : RedisStore::class;
|
||||
$storeClass = str_starts_with($connection, 'memcached:') ? MemcachedStore::class : RedisStore::class;
|
||||
$connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);
|
||||
|
||||
return new $storeClass($connection);
|
||||
|
||||
case 0 === strpos($connection, 'mssql://'):
|
||||
case 0 === strpos($connection, 'mysql:'):
|
||||
case 0 === strpos($connection, 'mysql2://'):
|
||||
case 0 === strpos($connection, 'oci:'):
|
||||
case 0 === strpos($connection, 'oci8://'):
|
||||
case 0 === strpos($connection, 'pdo_oci://'):
|
||||
case 0 === strpos($connection, 'pgsql:'):
|
||||
case 0 === strpos($connection, 'postgres://'):
|
||||
case 0 === strpos($connection, 'postgresql://'):
|
||||
case 0 === strpos($connection, 'sqlsrv:'):
|
||||
case 0 === strpos($connection, 'sqlite:'):
|
||||
case 0 === strpos($connection, 'sqlite3://'):
|
||||
case str_starts_with($connection, 'mssql://'):
|
||||
case str_starts_with($connection, 'mysql:'):
|
||||
case str_starts_with($connection, 'mysql2://'):
|
||||
case str_starts_with($connection, 'oci:'):
|
||||
case str_starts_with($connection, 'oci8://'):
|
||||
case str_starts_with($connection, 'pdo_oci://'):
|
||||
case str_starts_with($connection, 'pgsql:'):
|
||||
case str_starts_with($connection, 'postgres://'):
|
||||
case str_starts_with($connection, 'postgresql://'):
|
||||
case str_starts_with($connection, 'sqlsrv:'):
|
||||
case str_starts_with($connection, 'sqlite:'):
|
||||
case str_starts_with($connection, 'sqlite3://'):
|
||||
return new PdoStore($connection);
|
||||
|
||||
case 0 === strpos($connection, 'zookeeper://'):
|
||||
case str_starts_with($connection, 'zookeeper://'):
|
||||
return new ZookeeperStore(ZookeeperStore::createConnection($connection));
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', $connection));
|
||||
throw new InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection));
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -37,12 +37,12 @@ class ZookeeperStore implements StoreInterface
|
||||
|
||||
public static function createConnection(string $dsn): \Zookeeper
|
||||
{
|
||||
if (0 !== strpos($dsn, 'zookeeper:')) {
|
||||
throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn));
|
||||
if (!str_starts_with($dsn, 'zookeeper:')) {
|
||||
throw new InvalidArgumentException(sprintf('Unsupported DSN: "%s".', $dsn));
|
||||
}
|
||||
|
||||
if (false === $params = parse_url($dsn)) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid Zookeeper DSN: %s.', $dsn));
|
||||
throw new InvalidArgumentException(sprintf('Invalid Zookeeper DSN: "%s".', $dsn));
|
||||
}
|
||||
|
||||
$host = $params['host'] ?? '';
|
||||
@@ -108,7 +108,7 @@ class ZookeeperStore implements StoreInterface
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), E_USER_DEPRECATED);
|
||||
@trigger_error(sprintf('%s() is deprecated since Symfony 4.4 and will be removed in Symfony 5.0.', __METHOD__), \E_USER_DEPRECATED);
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ class ZookeeperStore implements StoreInterface
|
||||
// For example: foo/bar will become /foo-bar and /foo/bar will become /-foo-bar
|
||||
$resource = (string) $key;
|
||||
|
||||
if (false !== strpos($resource, '/')) {
|
||||
if (str_contains($resource, '/')) {
|
||||
$resource = strtr($resource, ['/' => '-']).'-'.sha1($resource);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+7
-12
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "symfony/lock",
|
||||
"type": "library",
|
||||
"description": "Symfony Lock Component",
|
||||
"description": "Creates and manages locks, a mechanism to provide exclusive access to a shared resource",
|
||||
"keywords": ["locking", "redlock", "mutex", "semaphore", "flock", "cas"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
@@ -16,16 +16,16 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.1.3",
|
||||
"psr/log": "~1.0"
|
||||
"php": ">=7.1.3",
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/polyfill-php80": "^1.16"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "~2.5",
|
||||
"mongodb/mongodb": "~1.1",
|
||||
"doctrine/dbal": "^2.6|^3.0",
|
||||
"predis/predis": "~1.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/dbal": "<2.5"
|
||||
"doctrine/dbal": "<2.6"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Lock\\": "" },
|
||||
@@ -33,10 +33,5 @@
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.4-dev"
|
||||
}
|
||||
}
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
|
||||
+24
@@ -11,16 +11,40 @@
|
||||
|
||||
use Symfony\Polyfill\Ctype as p;
|
||||
|
||||
if (\PHP_VERSION_ID >= 80000) {
|
||||
return require __DIR__.'/bootstrap80.php';
|
||||
}
|
||||
|
||||
if (!function_exists('ctype_alnum')) {
|
||||
function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); }
|
||||
}
|
||||
if (!function_exists('ctype_alpha')) {
|
||||
function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); }
|
||||
}
|
||||
if (!function_exists('ctype_cntrl')) {
|
||||
function ctype_cntrl($text) { return p\Ctype::ctype_cntrl($text); }
|
||||
}
|
||||
if (!function_exists('ctype_digit')) {
|
||||
function ctype_digit($text) { return p\Ctype::ctype_digit($text); }
|
||||
}
|
||||
if (!function_exists('ctype_graph')) {
|
||||
function ctype_graph($text) { return p\Ctype::ctype_graph($text); }
|
||||
}
|
||||
if (!function_exists('ctype_lower')) {
|
||||
function ctype_lower($text) { return p\Ctype::ctype_lower($text); }
|
||||
}
|
||||
if (!function_exists('ctype_print')) {
|
||||
function ctype_print($text) { return p\Ctype::ctype_print($text); }
|
||||
}
|
||||
if (!function_exists('ctype_punct')) {
|
||||
function ctype_punct($text) { return p\Ctype::ctype_punct($text); }
|
||||
}
|
||||
if (!function_exists('ctype_space')) {
|
||||
function ctype_space($text) { return p\Ctype::ctype_space($text); }
|
||||
}
|
||||
if (!function_exists('ctype_upper')) {
|
||||
function ctype_upper($text) { return p\Ctype::ctype_upper($text); }
|
||||
}
|
||||
if (!function_exists('ctype_xdigit')) {
|
||||
function ctype_xdigit($text) { return p\Ctype::ctype_xdigit($text); }
|
||||
}
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?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.
|
||||
*/
|
||||
|
||||
use Symfony\Polyfill\Ctype as p;
|
||||
|
||||
if (!function_exists('ctype_alnum')) {
|
||||
function ctype_alnum(mixed $text): bool { return p\Ctype::ctype_alnum($text); }
|
||||
}
|
||||
if (!function_exists('ctype_alpha')) {
|
||||
function ctype_alpha(mixed $text): bool { return p\Ctype::ctype_alpha($text); }
|
||||
}
|
||||
if (!function_exists('ctype_cntrl')) {
|
||||
function ctype_cntrl(mixed $text): bool { return p\Ctype::ctype_cntrl($text); }
|
||||
}
|
||||
if (!function_exists('ctype_digit')) {
|
||||
function ctype_digit(mixed $text): bool { return p\Ctype::ctype_digit($text); }
|
||||
}
|
||||
if (!function_exists('ctype_graph')) {
|
||||
function ctype_graph(mixed $text): bool { return p\Ctype::ctype_graph($text); }
|
||||
}
|
||||
if (!function_exists('ctype_lower')) {
|
||||
function ctype_lower(mixed $text): bool { return p\Ctype::ctype_lower($text); }
|
||||
}
|
||||
if (!function_exists('ctype_print')) {
|
||||
function ctype_print(mixed $text): bool { return p\Ctype::ctype_print($text); }
|
||||
}
|
||||
if (!function_exists('ctype_punct')) {
|
||||
function ctype_punct(mixed $text): bool { return p\Ctype::ctype_punct($text); }
|
||||
}
|
||||
if (!function_exists('ctype_space')) {
|
||||
function ctype_space(mixed $text): bool { return p\Ctype::ctype_space($text); }
|
||||
}
|
||||
if (!function_exists('ctype_upper')) {
|
||||
function ctype_upper(mixed $text): bool { return p\Ctype::ctype_upper($text); }
|
||||
}
|
||||
if (!function_exists('ctype_xdigit')) {
|
||||
function ctype_xdigit(mixed $text): bool { return p\Ctype::ctype_xdigit($text); }
|
||||
}
|
||||
+6
-2
@@ -16,7 +16,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Polyfill\\Ctype\\": "" },
|
||||
@@ -28,7 +28,11 @@
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.15-dev"
|
||||
"dev-main": "1.23-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+247
File diff suppressed because one or more lines are too long
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2015-2019 Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
Symfony Polyfill / Intl: Grapheme
|
||||
=================================
|
||||
|
||||
This component provides a partial, native PHP implementation of the
|
||||
[Grapheme functions](https://php.net/intl.grapheme) from the
|
||||
[Intl](https://php.net/intl) extension.
|
||||
|
||||
- [`grapheme_extract`](https://php.net/grapheme_extract): Extract a sequence of grapheme
|
||||
clusters from a text buffer, which must be encoded in UTF-8
|
||||
- [`grapheme_stripos`](https://php.net/grapheme_stripos): Find position (in grapheme units)
|
||||
of first occurrence of a case-insensitive string
|
||||
- [`grapheme_stristr`](https://php.net/grapheme_stristr): Returns part of haystack string
|
||||
from the first occurrence of case-insensitive needle to the end of haystack
|
||||
- [`grapheme_strlen`](https://php.net/grapheme_strlen): Get string length in grapheme units
|
||||
- [`grapheme_strpos`](https://php.net/grapheme_strpos): Find position (in grapheme units)
|
||||
of first occurrence of a string
|
||||
- [`grapheme_strripos`](https://php.net/grapheme_strripos): Find position (in grapheme units)
|
||||
of last occurrence of a case-insensitive string
|
||||
- [`grapheme_strrpos`](https://php.net/grapheme_strrpos): Find position (in grapheme units)
|
||||
of last occurrence of a string
|
||||
- [`grapheme_strstr`](https://php.net/grapheme_strstr): Returns part of haystack string from
|
||||
the first occurrence of needle to the end of haystack
|
||||
- [`grapheme_substr`](https://php.net/grapheme_substr): Return part of a string
|
||||
|
||||
More information can be found in the
|
||||
[main Polyfill README](https://github.com/symfony/polyfill/blob/master/README.md).
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
This library is released under the [MIT license](LICENSE).
|
||||
@@ -0,0 +1,58 @@
|
||||
<?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.
|
||||
*/
|
||||
|
||||
use Symfony\Polyfill\Intl\Grapheme as p;
|
||||
|
||||
if (extension_loaded('intl')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (\PHP_VERSION_ID >= 80000) {
|
||||
return require __DIR__.'/bootstrap80.php';
|
||||
}
|
||||
|
||||
if (!defined('GRAPHEME_EXTR_COUNT')) {
|
||||
define('GRAPHEME_EXTR_COUNT', 0);
|
||||
}
|
||||
if (!defined('GRAPHEME_EXTR_MAXBYTES')) {
|
||||
define('GRAPHEME_EXTR_MAXBYTES', 1);
|
||||
}
|
||||
if (!defined('GRAPHEME_EXTR_MAXCHARS')) {
|
||||
define('GRAPHEME_EXTR_MAXCHARS', 2);
|
||||
}
|
||||
|
||||
if (!function_exists('grapheme_extract')) {
|
||||
function grapheme_extract($haystack, $size, $type = 0, $start = 0, &$next = 0) { return p\Grapheme::grapheme_extract($haystack, $size, $type, $start, $next); }
|
||||
}
|
||||
if (!function_exists('grapheme_stripos')) {
|
||||
function grapheme_stripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_stripos($haystack, $needle, $offset); }
|
||||
}
|
||||
if (!function_exists('grapheme_stristr')) {
|
||||
function grapheme_stristr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_stristr($haystack, $needle, $beforeNeedle); }
|
||||
}
|
||||
if (!function_exists('grapheme_strlen')) {
|
||||
function grapheme_strlen($input) { return p\Grapheme::grapheme_strlen($input); }
|
||||
}
|
||||
if (!function_exists('grapheme_strpos')) {
|
||||
function grapheme_strpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strpos($haystack, $needle, $offset); }
|
||||
}
|
||||
if (!function_exists('grapheme_strripos')) {
|
||||
function grapheme_strripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strripos($haystack, $needle, $offset); }
|
||||
}
|
||||
if (!function_exists('grapheme_strrpos')) {
|
||||
function grapheme_strrpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strrpos($haystack, $needle, $offset); }
|
||||
}
|
||||
if (!function_exists('grapheme_strstr')) {
|
||||
function grapheme_strstr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_strstr($haystack, $needle, $beforeNeedle); }
|
||||
}
|
||||
if (!function_exists('grapheme_substr')) {
|
||||
function grapheme_substr($string, $offset, $length = null) { return p\Grapheme::grapheme_substr($string, $offset, $length); }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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.
|
||||
*/
|
||||
|
||||
use Symfony\Polyfill\Intl\Grapheme as p;
|
||||
|
||||
if (!defined('GRAPHEME_EXTR_COUNT')) {
|
||||
define('GRAPHEME_EXTR_COUNT', 0);
|
||||
}
|
||||
if (!defined('GRAPHEME_EXTR_MAXBYTES')) {
|
||||
define('GRAPHEME_EXTR_MAXBYTES', 1);
|
||||
}
|
||||
if (!defined('GRAPHEME_EXTR_MAXCHARS')) {
|
||||
define('GRAPHEME_EXTR_MAXCHARS', 2);
|
||||
}
|
||||
|
||||
if (!function_exists('grapheme_extract')) {
|
||||
function grapheme_extract(?string $haystack, ?int $size, ?int $type = GRAPHEME_EXTR_COUNT, ?int $offset = 0, &$next = null): string|false { return p\Grapheme::grapheme_extract((string) $haystack, (int) $size, (int) $type, (int) $offset, $next); }
|
||||
}
|
||||
if (!function_exists('grapheme_stripos')) {
|
||||
function grapheme_stripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_stripos((string) $haystack, (string) $needle, (int) $offset); }
|
||||
}
|
||||
if (!function_exists('grapheme_stristr')) {
|
||||
function grapheme_stristr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_stristr((string) $haystack, (string) $needle, (bool) $beforeNeedle); }
|
||||
}
|
||||
if (!function_exists('grapheme_strlen')) {
|
||||
function grapheme_strlen(?string $string): int|false|null { return p\Grapheme::grapheme_strlen((string) $string); }
|
||||
}
|
||||
if (!function_exists('grapheme_strpos')) {
|
||||
function grapheme_strpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strpos((string) $haystack, (string) $needle, (int) $offset); }
|
||||
}
|
||||
if (!function_exists('grapheme_strripos')) {
|
||||
function grapheme_strripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strripos((string) $haystack, (string) $needle, (int) $offset); }
|
||||
}
|
||||
if (!function_exists('grapheme_strrpos')) {
|
||||
function grapheme_strrpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strrpos((string) $haystack, (string) $needle, (int) $offset); }
|
||||
}
|
||||
if (!function_exists('grapheme_strstr')) {
|
||||
function grapheme_strstr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_strstr((string) $haystack, (string) $needle, (bool) $beforeNeedle); }
|
||||
}
|
||||
if (!function_exists('grapheme_substr')) {
|
||||
function grapheme_substr(?string $string, ?int $offset, ?int $length = null): string|false { return p\Grapheme::grapheme_substr((string) $string, (int) $offset, $length); }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "symfony/polyfill-intl-grapheme",
|
||||
"type": "library",
|
||||
"description": "Symfony polyfill for intl's grapheme_* functions",
|
||||
"keywords": ["polyfill", "shim", "compatibility", "portable", "intl", "grapheme"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Polyfill\\Intl\\Grapheme\\": "" },
|
||||
"files": [ "bootstrap.php" ]
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "For best performance"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.23-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "symfony/polyfill",
|
||||
"url": "https://github.com/symfony/polyfill"
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user