PHP 8 Compatibility

This commit is contained in:
Frédéric Guillot
2022-02-05 11:49:03 -08:00
committed by GitHub
parent 61e63ef9e0
commit f5bb55bdb8
558 changed files with 6262 additions and 21691 deletions

View File

@@ -107,12 +107,7 @@ class DebugFormatterHelper extends Helper
return $message;
}
/**
* @param string $id The id of the formatting session
*
* @return string
*/
private function getBorder($id)
private function getBorder(string $id): string
{
return sprintf('<bg=%s> </>', $this->colors[$this->started[$id]['border']]);
}

View File

@@ -48,9 +48,7 @@ class DescriptorHelper extends Helper
* * format: string, the output format name
* * raw_text: boolean, sets output type as raw
*
* @param OutputInterface $output
* @param object $object
* @param array $options
* @param object $object
*
* @throws InvalidArgumentException when the given format is not supported
*/
@@ -72,8 +70,7 @@ class DescriptorHelper extends Helper
/**
* Registers a descriptor.
*
* @param string $format
* @param DescriptorInterface $descriptor
* @param string $format
*
* @return $this
*/

View File

@@ -0,0 +1,64 @@
<?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\Output\OutputInterface;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
/**
* @author Roland Franssen <franssen.roland@gmail.com>
*/
final class Dumper
{
private $output;
private $dumper;
private $cloner;
private $handler;
public function __construct(OutputInterface $output, CliDumper $dumper = null, ClonerInterface $cloner = null)
{
$this->output = $output;
$this->dumper = $dumper;
$this->cloner = $cloner;
if (class_exists(CliDumper::class)) {
$this->handler = function ($var): string {
$dumper = $this->dumper ?? $this->dumper = new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR);
$dumper->setColors($this->output->isDecorated());
return rtrim($dumper->dump(($this->cloner ?? $this->cloner = new VarCloner())->cloneVar($var)->withRefHandles(false), true));
};
} else {
$this->handler = function ($var): string {
switch (true) {
case null === $var:
return 'null';
case true === $var:
return 'true';
case false === $var:
return 'false';
case \is_string($var):
return '"'.$var.'"';
default:
return rtrim(print_r($var, true));
}
};
}
}
public function __invoke($var): string
{
return ($this->handler)($var);
}
}

View File

@@ -54,12 +54,12 @@ class FormatterHelper extends Helper
foreach ($messages as $message) {
$message = OutputFormatter::escape($message);
$lines[] = sprintf($large ? ' %s ' : ' %s ', $message);
$len = max($this->strlen($message) + ($large ? 4 : 2), $len);
$len = max(self::strlen($message) + ($large ? 4 : 2), $len);
}
$messages = $large ? [str_repeat(' ', $len)] : [];
for ($i = 0; isset($lines[$i]); ++$i) {
$messages[] = $lines[$i].str_repeat(' ', $len - $this->strlen($lines[$i]));
$messages[] = $lines[$i].str_repeat(' ', $len - self::strlen($lines[$i]));
}
if ($large) {
$messages[] = str_repeat(' ', $len);
@@ -83,17 +83,13 @@ class FormatterHelper extends Helper
*/
public function truncate($message, $length, $suffix = '...')
{
$computedLength = $length - $this->strlen($suffix);
$computedLength = $length - self::strlen($suffix);
if ($computedLength > $this->strlen($message)) {
if ($computedLength > self::strlen($message)) {
return $message;
}
if (false === $encoding = mb_detect_encoding($message, null, true)) {
return substr($message, 0, $length).$suffix;
}
return mb_substr($message, 0, $length, $encoding).$suffix;
return self::substr($message, 0, $length).$suffix;
}
/**

View File

@@ -47,6 +47,8 @@ abstract class Helper implements HelperInterface
*/
public static function strlen($string)
{
$string = (string) $string;
if (false === $encoding = mb_detect_encoding($string, null, true)) {
return \strlen($string);
}
@@ -65,6 +67,8 @@ abstract class Helper implements HelperInterface
*/
public static function substr($string, $from, $length = null)
{
$string = (string) $string;
if (false === $encoding = mb_detect_encoding($string, null, true)) {
return substr($string, $from, $length);
}

View File

@@ -40,8 +40,7 @@ class HelperSet implements \IteratorAggregate
/**
* Sets a helper.
*
* @param HelperInterface $helper The helper instance
* @param string $alias An alias
* @param string $alias An alias
*/
public function set(HelperInterface $helper, $alias = null)
{
@@ -99,8 +98,9 @@ class HelperSet implements \IteratorAggregate
}
/**
* @return Helper[]
* @return \Traversable<Helper>
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator($this->helpers);

View File

@@ -28,17 +28,20 @@ class ProcessHelper extends Helper
/**
* Runs an external process.
*
* @param OutputInterface $output An OutputInterface instance
* @param array|Process $cmd An instance of Process or an array of the command and arguments
* @param string|null $error An error message that must be displayed if something went wrong
* @param callable|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
* @param int $verbosity The threshold for verbosity
* @param array|Process $cmd An instance of Process or an array of the command and arguments
* @param string|null $error An error message that must be displayed if something went wrong
* @param callable|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
* @param int $verbosity The threshold for verbosity
*
* @return Process The process that ran
*/
public function run(OutputInterface $output, $cmd, $error = null, callable $callback = null, $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE)
{
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();
}
@@ -50,7 +53,7 @@ class ProcessHelper extends Helper
}
if (!\is_array($cmd)) {
@trigger_error(sprintf('Passing a command as a string to "%s()" is deprecated since Symfony 4.2, pass it the command as an array of arguments instead.', __METHOD__), E_USER_DEPRECATED);
@trigger_error(sprintf('Passing a command as a string to "%s()" is deprecated since Symfony 4.2, pass it the command as an array of arguments instead.', __METHOD__), \E_USER_DEPRECATED);
$cmd = [method_exists(Process::class, 'fromShellCommandline') ? Process::fromShellCommandline($cmd) : new Process($cmd)];
}
@@ -92,11 +95,10 @@ class ProcessHelper extends Helper
* This is identical to run() except that an exception is thrown if the process
* exits with a non-zero exit code.
*
* @param OutputInterface $output An OutputInterface instance
* @param string|Process $cmd An instance of Process or a command to run
* @param string|null $error An error message that must be displayed if something went wrong
* @param callable|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
* @param array|Process $cmd An instance of Process or a command to run
* @param string|null $error An error message that must be displayed if something went wrong
* @param callable|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
*
* @return Process The process that ran
*
@@ -118,10 +120,6 @@ class ProcessHelper extends Helper
/**
* Wraps a Process callback to add debugging output.
*
* @param OutputInterface $output An OutputInterface interface
* @param Process $process The Process
* @param callable|null $callback A PHP callable
*
* @return callable
*/
public function wrapCallback(OutputInterface $output, Process $process, callable $callback = null)
@@ -141,7 +139,7 @@ class ProcessHelper extends Helper
};
}
private function escapeString($str)
private function escapeString(string $str): string
{
return str_replace('<', '\\<', $str);
}

View File

@@ -32,6 +32,10 @@ final class ProgressBar
private $format;
private $internalFormat;
private $redrawFreq = 1;
private $writeCount;
private $lastWriteTime;
private $minSecondsBetweenRedraws = 0;
private $maxSecondsBetweenRedraws = 1;
private $output;
private $step = 0;
private $max;
@@ -42,16 +46,15 @@ final class ProgressBar
private $messages = [];
private $overwrite = true;
private $terminal;
private $firstRun = true;
private $previousMessage;
private static $formatters;
private static $formats;
/**
* @param OutputInterface $output An OutputInterface instance
* @param int $max Maximum steps (0 if unknown)
* @param int $max Maximum steps (0 if unknown)
*/
public function __construct(OutputInterface $output, int $max = 0)
public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 0.1)
{
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
@@ -61,12 +64,17 @@ final class ProgressBar
$this->setMaxSteps($max);
$this->terminal = new Terminal();
if (0 < $minSecondsBetweenRedraws) {
$this->redrawFreq = null;
$this->minSecondsBetweenRedraws = $minSecondsBetweenRedraws;
}
if (!$this->output->isDecorated()) {
// disable overwrite when output does not support ANSI codes.
$this->overwrite = false;
// set a reasonable redraw frequency so output isn't flooded
$this->setRedrawFrequency($max / 10);
$this->redrawFreq = null;
}
$this->startTime = time();
@@ -102,7 +110,7 @@ final class ProgressBar
self::$formatters = self::initPlaceholderFormatters();
}
return isset(self::$formatters[$name]) ? self::$formatters[$name] : null;
return self::$formatters[$name] ?? null;
}
/**
@@ -135,7 +143,7 @@ final class ProgressBar
self::$formats = self::initFormats();
}
return isset(self::$formats[$name]) ? self::$formats[$name] : null;
return self::$formats[$name] ?? null;
}
/**
@@ -183,6 +191,11 @@ final class ProgressBar
return $this->percent;
}
public function getBarOffset(): int
{
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 setBarWidth(int $size)
{
$this->barWidth = max(1, $size);
@@ -236,11 +249,39 @@ 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)
public function setRedrawFrequency(?int $freq)
{
$this->redrawFreq = max($freq, 1);
$this->redrawFreq = null !== $freq ? max(1, $freq) : null;
}
public function minSecondsBetweenRedraws(float $seconds): void
{
$this->minSecondsBetweenRedraws = $seconds;
}
public function maxSecondsBetweenRedraws(float $seconds): void
{
$this->maxSecondsBetweenRedraws = $seconds;
}
/**
* Returns an iterator that will automatically update the progress bar when iterated.
*
* @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
*/
public function iterate(iterable $iterable, int $max = null): iterable
{
$this->start($max ?? (is_countable($iterable) ? \count($iterable) : 0));
foreach ($iterable as $key => $value) {
yield $key => $value;
$this->advance();
}
$this->finish();
}
/**
@@ -287,11 +328,27 @@ final class ProgressBar
$step = 0;
}
$prevPeriod = (int) ($this->step / $this->redrawFreq);
$currPeriod = (int) ($step / $this->redrawFreq);
$redrawFreq = $this->redrawFreq ?? (($this->max ?: 10) / 10);
$prevPeriod = (int) ($this->step / $redrawFreq);
$currPeriod = (int) ($step / $redrawFreq);
$this->step = $step;
$this->percent = $this->max ? (float) $this->step / $this->max : 0;
if ($prevPeriod !== $currPeriod || $this->max === $step) {
$timeInterval = microtime(true) - $this->lastWriteTime;
// Draw regardless of other limits
if ($this->max === $step) {
$this->display();
return;
}
// Throttling
if ($timeInterval < $this->minSecondsBetweenRedraws) {
return;
}
// Draw each step period, but not too late
if ($prevPeriod !== $currPeriod || $timeInterval >= $this->maxSecondsBetweenRedraws) {
$this->display();
}
}
@@ -375,11 +432,24 @@ final class ProgressBar
*/
private function overwrite(string $message): void
{
if ($this->previousMessage === $message) {
return;
}
$originalMessage = $message;
if ($this->overwrite) {
if (!$this->firstRun) {
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::strlenWithoutDecoration($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) {
@@ -391,12 +461,14 @@ final class ProgressBar
}
}
} elseif ($this->step > 0) {
$message = PHP_EOL.$message;
$message = \PHP_EOL.$message;
}
$this->firstRun = false;
$this->previousMessage = $originalMessage;
$this->lastWriteTime = microtime(true);
$this->output->write($message);
++$this->writeCount;
}
private function determineBestFormat(): string
@@ -418,7 +490,7 @@ final class ProgressBar
{
return [
'bar' => function (self $bar, OutputInterface $output) {
$completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getProgress() % $bar->getBarWidth());
$completeBars = $bar->getBarOffset();
$display = str_repeat($bar->getBarCharacter(), $completeBars);
if ($completeBars < $bar->getBarWidth()) {
$emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter());
@@ -460,7 +532,7 @@ final class ProgressBar
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();

View File

@@ -34,10 +34,9 @@ class ProgressIndicator
private static $formats;
/**
* @param OutputInterface $output
* @param string|null $format Indicator format
* @param int $indicatorChangeInterval Change interval in milliseconds
* @param array|null $indicatorValues Animated indicator characters
* @param string|null $format Indicator format
* @param int $indicatorChangeInterval Change interval in milliseconds
* @param array|null $indicatorValues Animated indicator characters
*/
public function __construct(OutputInterface $output, string $format = null, int $indicatorChangeInterval = 100, array $indicatorValues = null)
{
@@ -150,7 +149,7 @@ class ProgressIndicator
self::$formats = self::initFormats();
}
return isset(self::$formats[$name]) ? self::$formats[$name] : null;
return self::$formats[$name] ?? null;
}
/**
@@ -183,7 +182,7 @@ class ProgressIndicator
self::$formatters = self::initPlaceholderFormatters();
}
return isset(self::$formatters[$name]) ? self::$formatters[$name] : null;
return self::$formatters[$name] ?? null;
}
private function display()
@@ -192,18 +191,16 @@ class ProgressIndicator
return;
}
$self = $this;
$this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self) {
if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) {
return $formatter($self);
$this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) {
if ($formatter = self::getPlaceholderFormatterDefinition($matches[1])) {
return $formatter($this);
}
return $matches[0];
}, $this->format));
}, $this->format ?? ''));
}
private function determineBestFormat()
private function determineBestFormat(): string
{
switch ($this->output->getVerbosity()) {
// OutputInterface::VERBOSITY_QUIET: display is disabled anyway
@@ -230,12 +227,12 @@ class ProgressIndicator
}
}
private function getCurrentTimeInMilliseconds()
private function getCurrentTimeInMilliseconds(): float
{
return round(microtime(true) * 1000);
}
private static function initPlaceholderFormatters()
private static function initPlaceholderFormatters(): array
{
return [
'indicator' => function (self $indicator) {
@@ -253,7 +250,7 @@ class ProgressIndicator
];
}
private static function initFormats()
private static function initFormats(): array
{
return [
'normal' => ' %indicator% %message%',

View File

@@ -11,6 +11,7 @@
namespace Symfony\Component\Console\Helper;
use Symfony\Component\Console\Exception\MissingInputException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
@@ -21,6 +22,7 @@ use Symfony\Component\Console\Output\ConsoleSectionOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Terminal;
/**
* The QuestionHelper class provides helpers to interact with the user.
@@ -31,7 +33,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.
@@ -47,44 +50,32 @@ class QuestionHelper extends Helper
}
if (!$input->isInteractive()) {
$default = $question->getDefault();
if (null === $default) {
return $default;
}
if ($validator = $question->getValidator()) {
return \call_user_func($question->getValidator(), $default);
} elseif ($question instanceof ChoiceQuestion) {
$choices = $question->getChoices();
if (!$question->isMultiselect()) {
return isset($choices[$default]) ? $choices[$default] : $default;
}
$default = explode(',', $default);
foreach ($default as $k => $v) {
$v = trim($v);
$default[$k] = isset($choices[$v]) ? $choices[$v] : $v;
}
}
return $default;
return $this->getDefaultAnswer($question);
}
if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) {
$this->inputStream = $stream;
}
if (!$question->getValidator()) {
return $this->doAsk($output, $question);
try {
if (!$question->getValidator()) {
return $this->doAsk($output, $question);
}
$interviewer = function () use ($output, $question) {
return $this->doAsk($output, $question);
};
return $this->validateAttempts($interviewer, $output, $question);
} catch (MissingInputException $exception) {
$input->setInteractive(false);
if (null === $fallbackOutput = $this->getDefaultAnswer($question)) {
throw $exception;
}
return $fallbackOutput;
}
$interviewer = function () use ($output, $question) {
return $this->doAsk($output, $question);
};
return $this->validateAttempts($interviewer, $output, $question);
}
/**
@@ -106,7 +97,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
*/
@@ -114,14 +105,15 @@ class QuestionHelper extends Helper
{
$this->writePrompt($output, $question);
$inputStream = $this->inputStream ?: STDIN;
$autocomplete = $question->getAutocompleterValues();
$inputStream = $this->inputStream ?: \STDIN;
$autocomplete = $question->getAutocompleterCallback();
if (null === $autocomplete || !$this->hasSttyAvailable()) {
if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) {
$ret = false;
if ($question->isHidden()) {
try {
$ret = trim($this->getHiddenResponse($output, $inputStream));
$hiddenResponse = $this->getHiddenResponse($output, $inputStream, $question->isTrimmable());
$ret = $question->isTrimmable() ? trim($hiddenResponse) : $hiddenResponse;
} catch (RuntimeException $e) {
if (!$question->isHiddenFallback()) {
throw $e;
@@ -130,14 +122,19 @@ class QuestionHelper extends Helper
}
if (false === $ret) {
$cp = $this->setIOCodepage();
$ret = fgets($inputStream, 4096);
$ret = $this->resetIOCodepage($cp, $ret);
if (false === $ret) {
throw new RuntimeException('Aborted.');
throw new MissingInputException('Aborted.');
}
if ($question->isTrimmable()) {
$ret = trim($ret);
}
$ret = trim($ret);
}
} else {
$ret = trim($this->autocomplete($output, $question, $inputStream, \is_array($autocomplete) ? $autocomplete : iterator_to_array($autocomplete, false)));
$autocomplete = $this->autocomplete($output, $question, $inputStream, $autocomplete);
$ret = $question->isTrimmable() ? trim($autocomplete) : $autocomplete;
}
if ($output instanceof ConsoleSectionOutput) {
@@ -153,6 +150,36 @@ class QuestionHelper extends Helper
return $ret;
}
/**
* @return mixed
*/
private function getDefaultAnswer(Question $question)
{
$default = $question->getDefault();
if (null === $default) {
return $default;
}
if ($validator = $question->getValidator()) {
return \call_user_func($question->getValidator(), $default);
} elseif ($question instanceof ChoiceQuestion) {
$choices = $question->getChoices();
if (!$question->isMultiselect()) {
return $choices[$default] ?? $default;
}
$default = explode(',', $default);
foreach ($default as $k => $v) {
$v = $question->isTrimmable() ? trim($v) : $v;
$default[$k] = $choices[$v] ?? $v;
}
}
return $default;
}
/**
* Outputs the question prompt.
*/
@@ -161,15 +188,9 @@ class QuestionHelper extends Helper
$message = $question->getQuestion();
if ($question instanceof ChoiceQuestion) {
$maxWidth = max(array_map([$this, 'strlen'], array_keys($question->getChoices())));
$messages = (array) $question->getQuestion();
foreach ($question->getChoices() as $key => $value) {
$width = $maxWidth - $this->strlen($key);
$messages[] = ' [<info>'.$key.str_repeat(' ', $width).'</info>] '.$value;
}
$output->writeln($messages);
$output->writeln(array_merge([
$question->getQuestion(),
], $this->formatChoiceQuestionChoices($question, 'info')));
$message = $question->getPrompt();
}
@@ -177,6 +198,26 @@ class QuestionHelper extends Helper
$output->write($message);
}
/**
* @param string $tag
*
* @return string[]
*/
protected function formatChoiceQuestionChoices(ChoiceQuestion $question, $tag)
{
$messages = [];
$maxWidth = max(array_map([__CLASS__, 'strlen'], array_keys($choices = $question->getChoices())));
foreach ($choices as $key => $value) {
$padding = str_repeat(' ', $maxWidth - self::strlen($key));
$messages[] = sprintf(" [<$tag>%s$padding</$tag>] %s", $key, $value);
}
return $messages;
}
/**
* Outputs an error message.
*/
@@ -194,18 +235,16 @@ class QuestionHelper extends Helper
/**
* Autocompletes a question.
*
* @param OutputInterface $output
* @param Question $question
* @param resource $inputStream
* @param resource $inputStream
*/
private function autocomplete(OutputInterface $output, Question $question, $inputStream, array $autocomplete): string
private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string
{
$fullChoice = '';
$ret = '';
$i = 0;
$ofs = -1;
$matches = $autocomplete;
$matches = $autocomplete($ret);
$numMatches = \count($matches);
$sttyMode = shell_exec('stty -g');
@@ -223,25 +262,25 @@ class QuestionHelper extends Helper
// as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) {
shell_exec(sprintf('stty %s', $sttyMode));
throw new RuntimeException('Aborted.');
throw new MissingInputException('Aborted.');
} elseif ("\177" === $c) { // Backspace Character
if (0 === $numMatches && 0 !== $i) {
--$i;
$fullChoice = substr($fullChoice, 0, -1);
$fullChoice = self::substr($fullChoice, 0, $i);
// Move cursor backwards
$output->write("\033[1D");
}
if (0 === $i) {
$ofs = -1;
$matches = $autocomplete;
$matches = $autocomplete($ret);
$numMatches = \count($matches);
} else {
$numMatches = 0;
}
// Pop the last character off the end of our string
$ret = substr($ret, 0, $i);
$ret = self::substr($ret, 0, $i);
} elseif ("\033" === $c) {
// Did we read an escape sequence?
$c .= fread($inputStream, 2);
@@ -262,12 +301,21 @@ class QuestionHelper extends Helper
} elseif (\ord($c) < 32) {
if ("\t" === $c || "\n" === $c) {
if ($numMatches > 0 && -1 !== $ofs) {
$ret = $matches[$ofs];
$ret = (string) $matches[$ofs];
// Echo out remaining chars for current match
$remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))));
$output->write($remainingCharacters);
$fullChoice .= $remainingCharacters;
$i = \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 || str_starts_with($match, $ret);
}
);
$numMatches = \count($matches);
$ofs = -1;
}
if ("\n" === $c) {
@@ -298,9 +346,9 @@ class QuestionHelper extends Helper
$numMatches = 0;
$ofs = 0;
foreach ($autocomplete as $value) {
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;
}
}
@@ -326,15 +374,15 @@ class QuestionHelper extends Helper
return $fullChoice;
}
private function mostRecentlyEnteredValue($entered)
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;
}
@@ -344,12 +392,12 @@ class QuestionHelper extends Helper
/**
* Gets a hidden response from user.
*
* @param OutputInterface $output An Output instance
* @param resource $inputStream The handler resource
* @param resource $inputStream The handler resource
* @param bool $trimmable Is the answer trimmable
*
* @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
*/
private function getHiddenResponse(OutputInterface $output, $inputStream): string
private function getHiddenResponse(OutputInterface $output, $inputStream, bool $trimmable = true): string
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$exe = __DIR__.'/../Resources/bin/hiddeninput.exe';
@@ -361,7 +409,8 @@ class QuestionHelper extends Helper
$exe = $tmpExe;
}
$value = rtrim(shell_exec($exe));
$sExec = shell_exec('"'.$exe.'"');
$value = $trimmable ? rtrim($sExec) : $sExec;
$output->writeln('');
if (isset($tmpExe)) {
@@ -371,41 +420,34 @@ class QuestionHelper extends Helper
return $value;
}
if ($this->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 RuntimeException('Aborted.');
}
if (false === $value) {
throw new MissingInputException('Aborted.');
}
if ($trimmable) {
$value = trim($value);
$output->writeln('');
return $value;
}
$output->writeln('');
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);
$value = rtrim(shell_exec($command));
$output->writeln('');
return $value;
}
throw new RuntimeException('Unable to hide the response.');
return $value;
}
/**
* Validates an attempt.
*
* @param callable $interviewer A callable that will ask for a question and return the result
* @param OutputInterface $output An Output instance
* @param Question $question A Question instance
* @param callable $interviewer A callable that will ask for a question and return the result
*
* @return mixed The validated response
*
@@ -415,6 +457,7 @@ class QuestionHelper extends Helper
{
$error = null;
$attempts = $question->getMaxAttempts();
while (null === $attempts || $attempts--) {
if (null !== $error) {
$this->writeError($output, $error);
@@ -431,44 +474,67 @@ 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;
}
/**
* 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;
}
/**
* Returns whether Stty is available or not.
*/
private function hasSttyAvailable(): bool
{
if (null !== self::$stty) {
return self::$stty;
}
exec('stty 2>&1', $output, $exitcode);
return self::$stty = 0 === $exitcode;
return $input;
}
}

View File

@@ -58,7 +58,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;
@@ -68,15 +68,15 @@ class SymfonyQuestionHelper extends QuestionHelper
$output->writeln($text);
if ($question instanceof ChoiceQuestion) {
$width = max(array_map('strlen', array_keys($question->getChoices())));
$prompt = ' > ';
foreach ($question->getChoices() as $key => $value) {
$output->writeln(sprintf(" [<comment>%-${width}s</comment>] %s", $key, $value));
}
if ($question instanceof ChoiceQuestion) {
$output->writeln($this->formatChoiceQuestionChoices($question, 'comment'));
$prompt = $question->getPrompt();
}
$output->write(' > ');
$output->write($prompt);
}
/**

View File

@@ -48,6 +48,7 @@ class Table
* Table rows.
*/
private $rows = [];
private $horizontal = false;
/**
* Column widths cache.
@@ -102,8 +103,7 @@ class Table
/**
* Sets a style definition.
*
* @param string $name The style name
* @param TableStyle $style A TableStyle instance
* @param string $name The style name
*/
public static function setStyleDefinition($name, TableStyle $style)
{
@@ -207,8 +207,6 @@ class Table
/**
* Sets the minimum width of all columns.
*
* @param array $widths
*
* @return $this
*/
public function setColumnWidths(array $widths)
@@ -325,6 +323,13 @@ class Table
return $this;
}
public function setHorizontal(bool $horizontal = true): self
{
$this->horizontal = $horizontal;
return $this;
}
/**
* Renders table to output.
*
@@ -340,14 +345,36 @@ class Table
*/
public function render()
{
$rows = array_merge($this->headers, [$divider = new TableSeparator()], $this->rows);
$divider = new TableSeparator();
if ($this->horizontal) {
$rows = [];
foreach ($this->headers[0] ?? [] as $i => $header) {
$rows[$i] = [$header];
foreach ($this->rows as $row) {
if ($row instanceof TableSeparator) {
continue;
}
if (isset($row[$i])) {
$rows[$i][] = $row[$i];
} elseif ($rows[$i][0] instanceof TableCell && $rows[$i][0]->getColspan() >= 2) {
// Noop, there is a "title"
} else {
$rows[$i][] = null;
}
}
}
} else {
$rows = array_merge($this->headers, [$divider], $this->rows);
}
$this->calculateNumberOfColumns($rows);
$rows = $this->buildTableRows($rows);
$this->calculateColumnsWidth($rows);
$isHeader = true;
$isFirstRow = false;
$isHeader = !$this->horizontal;
$isFirstRow = $this->horizontal;
$hasTitle = (bool) $this->headerTitle;
foreach ($rows as $row) {
if ($divider === $row) {
$isHeader = false;
@@ -365,15 +392,19 @@ 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());
} else {
$this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat());
}
$this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat());
}
$this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat());
@@ -401,13 +432,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;
@@ -425,7 +456,7 @@ class Table
$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 {
@@ -439,7 +470,7 @@ class Table
/**
* Renders vertical column separator.
*/
private function renderColumnSeparator($type = self::BORDER_OUTSIDE)
private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string
{
$borders = $this->style->getBorderChars();
@@ -453,13 +484,17 @@ class Table
*
* | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
*/
private function renderRow(array $row, string $cellFormat)
private function renderRow(array $row, string $cellFormat, string $firstCellFormat = null)
{
$rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE);
$columns = $this->getRowColumns($row);
$last = \count($columns) - 1;
foreach ($columns as $i => $column) {
$rowContent .= $this->renderCell($row, $column, $cellFormat);
if ($firstCellFormat && 0 === $i) {
$rowContent .= $this->renderCell($row, $column, $firstCellFormat);
} else {
$rowContent .= $this->renderCell($row, $column, $cellFormat);
}
$rowContent .= $this->renderColumnSeparator($last === $i ? self::BORDER_OUTSIDE : self::BORDER_INSIDE);
}
$this->output->writeln($rowContent);
@@ -468,9 +503,9 @@ class Table
/**
* Renders table cell with padding.
*/
private function renderCell(array $row, int $column, string $cellFormat)
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).
@@ -499,7 +534,7 @@ class Table
/**
* Calculate number of columns for this table.
*/
private function calculateNumberOfColumns($rows)
private function calculateNumberOfColumns(array $rows)
{
$columns = [0];
foreach ($rows as $row) {
@@ -513,7 +548,7 @@ class Table
$this->numberOfColumns = max($columns);
}
private function buildTableRows($rows)
private function buildTableRows(array $rows): TableRows
{
/** @var WrappableOutputFormatterInterface $formatter */
$formatter = $this->output->getFormatter();
@@ -528,7 +563,7 @@ class Table
if (isset($this->columnMaxWidths[$column]) && Helper::strlenWithoutDecoration($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)));
@@ -541,19 +576,22 @@ 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;
}
}
}
}
return new TableRows(function () use ($rows, $unmergedRows) {
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);
}
}
}
@@ -568,7 +606,9 @@ class Table
++$numberOfRows; // Add row for header separator
}
++$numberOfRows; // Add row for footer separator
if (\count($this->rows) > 0) {
++$numberOfRows; // Add row for footer separator
}
return $numberOfRows;
}
@@ -583,7 +623,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.', \gettype($cell)));
}
if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
$nbLines = $cell->getRowspan() - 1;
@@ -599,7 +639,7 @@ class Table
// 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] : '';
$value = $lines[$unmergedRowKey - $line] ?? '';
$unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan()]);
if ($nbLines === $unmergedRowKey - $line) {
break;
@@ -632,9 +672,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) {
@@ -736,7 +777,7 @@ class Table
$cellWidth = Helper::strlenWithoutDecoration($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;
@@ -751,7 +792,7 @@ class Table
$this->numberOfColumns = null;
}
private static function initStyles()
private static function initStyles(): array
{
$borderless = new TableStyle();
$borderless
@@ -798,7 +839,7 @@ class Table
];
}
private function resolveStyle($name)
private function resolveStyle($name): TableStyle
{
if ($name instanceof TableStyle) {
return $name;

View File

@@ -23,7 +23,7 @@ class TableRows implements \IteratorAggregate
$this->generator = $generator;
}
public function getIterator()
public function getIterator(): \Traversable
{
$g = $this->generator;

View File

@@ -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.
@@ -58,7 +58,7 @@ class TableStyle
public function setPaddingChar($paddingChar)
{
if (!$paddingChar) {
throw new LogicException('The padding char must not be empty');
throw new LogicException('The padding char must not be empty.');
}
$this->paddingChar = $paddingChar;
@@ -112,7 +112,7 @@ class TableStyle
*/
public function setHorizontalBorderChar($horizontalBorderChar)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use setHorizontalBorderChars() instead.', __METHOD__), E_USER_DEPRECATED);
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use setHorizontalBorderChars() instead.', __METHOD__), \E_USER_DEPRECATED);
return $this->setHorizontalBorderChars($horizontalBorderChar, $horizontalBorderChar);
}
@@ -126,7 +126,7 @@ class TableStyle
*/
public function getHorizontalBorderChar()
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use getBorderChars() instead.', __METHOD__), E_USER_DEPRECATED);
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use getBorderChars() instead.', __METHOD__), \E_USER_DEPRECATED);
return $this->horizontalOutsideBorderChar;
}
@@ -168,7 +168,7 @@ class TableStyle
*/
public function setVerticalBorderChar($verticalBorderChar)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use setVerticalBorderChars() instead.', __METHOD__), E_USER_DEPRECATED);
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use setVerticalBorderChars() instead.', __METHOD__), \E_USER_DEPRECATED);
return $this->setVerticalBorderChars($verticalBorderChar, $verticalBorderChar);
}
@@ -182,7 +182,7 @@ class TableStyle
*/
public function getVerticalBorderChar()
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use getBorderChars() instead.', __METHOD__), E_USER_DEPRECATED);
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use getBorderChars() instead.', __METHOD__), \E_USER_DEPRECATED);
return $this->verticalOutsideBorderChar;
}
@@ -192,7 +192,7 @@ class TableStyle
*
* @internal
*/
public function getBorderChars()
public function getBorderChars(): array
{
return [
$this->horizontalOutsideBorderChar,
@@ -270,7 +270,7 @@ class TableStyle
*/
public function setCrossingChar($crossingChar)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1. Use setDefaultCrossingChar() instead.', __METHOD__), E_USER_DEPRECATED);
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1. Use setDefaultCrossingChar() instead.', __METHOD__), \E_USER_DEPRECATED);
return $this->setDefaultCrossingChar($crossingChar);
}
@@ -413,7 +413,7 @@ class TableStyle
*/
public function setPadType($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).');
}