Update to laravel 7

This commit is contained in:
KodeStar
2022-03-10 11:54:29 +00:00
parent 61a5a1a8b0
commit f9a19fce91
7170 changed files with 274189 additions and 283773 deletions

View File

@@ -20,22 +20,18 @@ namespace Symfony\Component\Console\Helper;
*/
class DebugFormatterHelper extends Helper
{
private $colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'default'];
private const COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'default'];
private $started = [];
private $count = -1;
/**
* Starts a debug formatting session.
*
* @param string $id The id of the formatting session
* @param string $message The message to display
* @param string $prefix The prefix to use
*
* @return string
*/
public function start($id, $message, $prefix = 'RUN')
public function start(string $id, string $message, string $prefix = 'RUN')
{
$this->started[$id] = ['border' => ++$this->count % \count($this->colors)];
$this->started[$id] = ['border' => ++$this->count % \count(self::COLORS)];
return sprintf("%s<bg=blue;fg=white> %s </> <fg=blue>%s</>\n", $this->getBorder($id), $prefix, $message);
}
@@ -43,15 +39,9 @@ class DebugFormatterHelper extends Helper
/**
* Adds progress to a formatting session.
*
* @param string $id The id of the formatting session
* @param string $buffer The message to display
* @param bool $error Whether to consider the buffer as error
* @param string $prefix The prefix for output
* @param string $errorPrefix The prefix for error output
*
* @return string
*/
public function progress($id, $buffer, $error = false, $prefix = 'OUT', $errorPrefix = 'ERR')
public function progress(string $id, string $buffer, bool $error = false, string $prefix = 'OUT', string $errorPrefix = 'ERR')
{
$message = '';
@@ -85,14 +75,9 @@ class DebugFormatterHelper extends Helper
/**
* Stops a formatting session.
*
* @param string $id The id of the formatting session
* @param string $message The message to display
* @param bool $successful Whether to consider the result as success
* @param string $prefix The prefix for the end output
*
* @return string
*/
public function stop($id, $message, $successful, $prefix = 'RES')
public function stop(string $id, string $message, bool $successful, string $prefix = 'RES')
{
$trailingEOL = isset($this->started[$id]['out']) || isset($this->started[$id]['err']) ? "\n" : '';
@@ -107,14 +92,9 @@ 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']]);
return sprintf('<bg=%s> </>', self::COLORS[$this->started[$id]['border']]);
}
/**

View File

@@ -48,13 +48,9 @@ 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
*
* @throws InvalidArgumentException when the given format is not supported
*/
public function describe(OutputInterface $output, $object, array $options = [])
public function describe(OutputInterface $output, ?object $object, array $options = [])
{
$options = array_merge([
'raw_text' => false,
@@ -72,12 +68,9 @@ class DescriptorHelper extends Helper
/**
* Registers a descriptor.
*
* @param string $format
* @param DescriptorInterface $descriptor
*
* @return $this
*/
public function register($format, DescriptorInterface $descriptor)
public function register(string $format, DescriptorInterface $descriptor)
{
$this->descriptors[$format] = $descriptor;
@@ -91,4 +84,9 @@ class DescriptorHelper extends Helper
{
return 'descriptor';
}
public function getFormats(): array
{
return array_keys($this->descriptors);
}
}

View File

@@ -23,13 +23,9 @@ class FormatterHelper extends Helper
/**
* Formats a message within a section.
*
* @param string $section The section name
* @param string $message The message
* @param string $style The style to apply to the section
*
* @return string The format section
* @return string
*/
public function formatSection($section, $message, $style = 'info')
public function formatSection(string $section, string $message, string $style = 'info')
{
return sprintf('<%s>[%s]</%s> %s', $style, $section, $style, $message);
}
@@ -38,12 +34,10 @@ class FormatterHelper extends Helper
* Formats a message as a block of text.
*
* @param string|array $messages The message to write in the block
* @param string $style The style to apply to the whole block
* @param bool $large Whether to return a large block
*
* @return string The formatter message
* @return string
*/
public function formatBlock($messages, $style, $large = false)
public function formatBlock($messages, string $style, bool $large = false)
{
if (!\is_array($messages)) {
$messages = [$messages];
@@ -54,12 +48,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::width($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::width($lines[$i]));
}
if ($large) {
$messages[] = str_repeat(' ', $len);
@@ -75,25 +69,17 @@ class FormatterHelper extends Helper
/**
* Truncates a message to the given length.
*
* @param string $message
* @param int $length
* @param string $suffix
*
* @return string
*/
public function truncate($message, $length, $suffix = '...')
public function truncate(string $message, int $length, string $suffix = '...')
{
$computedLength = $length - $this->strlen($suffix);
$computedLength = $length - self::width($suffix);
if ($computedLength > $this->strlen($message)) {
if ($computedLength > self::width($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

@@ -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,12 +42,29 @@ abstract class Helper implements HelperInterface
/**
* Returns the length of a string, using mb_strwidth if it is available.
*
* @param string $string The string to check its length
* @deprecated since Symfony 5.3
*
* @return int The length of the string
* @return int
*/
public static function strlen($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);
}
@@ -54,17 +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.
*
* @param string $string String to subset
* @param int $from Start offset
* @param int|null $length Length to read
*
* @return string The string subset
* @return string
*/
public static function substr($string, $from, $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);
}
@@ -101,7 +136,7 @@ abstract class Helper implements HelperInterface
}
}
public static function formatMemory($memory)
public static function formatMemory(int $memory)
{
if ($memory >= 1024 * 1024 * 1024) {
return sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024);
@@ -118,19 +153,24 @@ abstract class Helper implements HelperInterface
return sprintf('%d B', $memory);
}
public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, $string)
/**
* @deprecated since Symfony 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);
$string = preg_replace("/\033\[[^m]*m/", '', $string ?? '');
$formatter->setDecorated($isDecorated);
return $string;

View File

@@ -26,14 +26,14 @@ interface HelperInterface
/**
* Gets the helper set associated with this helper.
*
* @return HelperSet A HelperSet instance
* @return HelperSet|null
*/
public function getHelperSet();
/**
* Returns the canonical name of this helper.
*
* @return string The canonical name
* @return string
*/
public function getName();
}

View File

@@ -18,12 +18,12 @@ use Symfony\Component\Console\Exception\InvalidArgumentException;
* HelperSet represents a set of helpers to be used with a command.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @implements \IteratorAggregate<string, Helper>
*/
class HelperSet implements \IteratorAggregate
{
/**
* @var Helper[]
*/
/** @var array<string, Helper> */
private $helpers = [];
private $command;
@@ -37,13 +37,7 @@ class HelperSet implements \IteratorAggregate
}
}
/**
* Sets a helper.
*
* @param HelperInterface $helper The helper instance
* @param string $alias An alias
*/
public function set(HelperInterface $helper, $alias = null)
public function set(HelperInterface $helper, string $alias = null)
{
$this->helpers[$helper->getName()] = $helper;
if (null !== $alias) {
@@ -56,11 +50,9 @@ class HelperSet implements \IteratorAggregate
/**
* Returns true if the helper if defined.
*
* @param string $name The helper name
*
* @return bool true if the helper is defined, false otherwise
* @return bool
*/
public function has($name)
public function has(string $name)
{
return isset($this->helpers[$name]);
}
@@ -68,13 +60,11 @@ class HelperSet implements \IteratorAggregate
/**
* Gets a helper value.
*
* @param string $name The helper name
*
* @return HelperInterface The helper instance
* @return HelperInterface
*
* @throws InvalidArgumentException if the helper is not defined
*/
public function get($name)
public function get(string $name)
{
if (!$this->has($name)) {
throw new InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name));
@@ -83,24 +73,34 @@ class HelperSet implements \IteratorAggregate
return $this->helpers[$name];
}
/**
* @deprecated since Symfony 5.4
*/
public function setCommand(Command $command = null)
{
trigger_deprecation('symfony/console', '5.4', 'Method "%s()" is deprecated.', __METHOD__);
$this->command = $command;
}
/**
* Gets the command associated with this helper set.
*
* @return Command A Command instance
* @return Command
*
* @deprecated since Symfony 5.4
*/
public function getCommand()
{
trigger_deprecation('symfony/console', '5.4', 'Method "%s()" is deprecated.', __METHOD__);
return $this->command;
}
/**
* @return Helper[]
* @return \Traversable<string, Helper>
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator($this->helpers);

View File

@@ -21,24 +21,23 @@ use Symfony\Component\Process\Process;
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final since Symfony 4.2
* @final
*/
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
*
* @return Process The process that ran
* @param array|Process $cmd An instance of Process or an array of the command and arguments
* @param callable|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
*/
public function run(OutputInterface $output, $cmd, $error = null, callable $callback = null, $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE)
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();
}
@@ -50,8 +49,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);
$cmd = [\method_exists(Process::class, 'fromShellCommandline') ? Process::fromShellCommandline($cmd) : new Process($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)) {
@@ -92,19 +90,15 @@ 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
*
* @return Process The process that ran
* @param array|Process $cmd An instance of Process or a command to run
* @param callable|null $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
*
* @throws ProcessFailedException
*
* @see run()
*/
public function mustRun(OutputInterface $output, $cmd, $error = null, callable $callback = null)
public function mustRun(OutputInterface $output, $cmd, string $error = null, callable $callback = null): Process
{
$process = $this->run($output, $cmd, $error, $callback);
@@ -117,14 +111,8 @@ 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)
public function wrapCallback(OutputInterface $output, Process $process, callable $callback = null): callable
{
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
@@ -141,7 +129,7 @@ class ProcessHelper extends Helper
};
}
private function escapeString($str)
private function escapeString(string $str): string
{
return str_replace('<', '\\<', $str);
}
@@ -149,7 +137,7 @@ class ProcessHelper extends Helper
/**
* {@inheritdoc}
*/
public function getName()
public function getName(): string
{
return 'process';
}

View File

@@ -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 = '-';
@@ -32,6 +43,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 +57,16 @@ final class ProgressBar
private $messages = [];
private $overwrite = true;
private $terminal;
private $firstRun = true;
private $previousMessage;
private $cursor;
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 = 1 / 25)
{
if ($output instanceof ConsoleOutputInterface) {
$output = $output->getErrorOutput();
@@ -61,15 +76,21 @@ 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();
$this->cursor = new Cursor($output);
}
/**
@@ -93,8 +114,6 @@ final class ProgressBar
* Gets the placeholder formatter for a given name.
*
* @param string $name The placeholder name (including the delimiter char like %)
*
* @return callable|null A PHP callable
*/
public static function getPlaceholderFormatterDefinition(string $name): ?callable
{
@@ -102,7 +121,7 @@ final class ProgressBar
self::$formatters = self::initPlaceholderFormatters();
}
return isset(self::$formatters[$name]) ? self::$formatters[$name] : null;
return self::$formatters[$name] ?? null;
}
/**
@@ -126,8 +145,6 @@ final class ProgressBar
* Gets the format for a given name.
*
* @param string $name The format name
*
* @return string|null A format string
*/
public static function getFormatDefinition(string $name): ?string
{
@@ -135,7 +152,7 @@ final class ProgressBar
self::$formats = self::initFormats();
}
return isset(self::$formats[$name]) ? self::$formats[$name] : null;
return self::$formats[$name] ?? null;
}
/**
@@ -183,6 +200,29 @@ final class ProgressBar
return $this->percent;
}
public function getBarOffset(): float
{
return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth);
}
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)
{
$this->barWidth = max(1, $size);
@@ -200,11 +240,7 @@ final class ProgressBar
public function getBarCharacter(): string
{
if (null === $this->barChar) {
return $this->max ? '=' : $this->emptyBarChar;
}
return $this->barChar;
return $this->barChar ?? ($this->max ? '=' : $this->emptyBarChar);
}
public function setEmptyBarCharacter(string $char)
@@ -236,11 +272,21 @@ 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;
}
/**
@@ -248,9 +294,9 @@ final class ProgressBar
*
* @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
public function iterate(iterable $iterable, int $max = null): iterable
{
$this->start($max ?? (\is_countable($iterable) ? \count($iterable) : 0));
$this->start($max ?? (is_countable($iterable) ? \count($iterable) : 0));
foreach ($iterable as $key => $value) {
yield $key => $value;
@@ -305,11 +351,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();
}
}
@@ -318,7 +380,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;
}
/**
@@ -393,28 +455,44 @@ 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::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;
for ($i = 0; $i < $this->formatLineCount; ++$i) {
$this->cursor->moveToColumn(1);
$this->cursor->clearLine();
$this->cursor->moveUp();
}
// 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->firstRun = false;
$this->previousMessage = $originalMessage;
$this->lastWriteTime = microtime(true);
$this->output->write($message);
++$this->writeCount;
}
private function determineBestFormat(): string
@@ -422,13 +500,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;
}
}
@@ -436,10 +514,10 @@ 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());
$emptyBars = $bar->getBarWidth() - $completeBars - Helper::length(Helper::removeDecoration($output->getFormatter(), $bar->getProgressCharacter()));
$display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars);
}
@@ -453,32 +531,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();
@@ -492,17 +558,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%',
];
}
@@ -528,7 +594,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);

View File

@@ -20,6 +20,17 @@ use Symfony\Component\Console\Output\OutputInterface;
*/
class ProgressIndicator
{
private const FORMATS = [
'normal' => ' %indicator% %message%',
'normal_no_ansi' => ' %message%',
'verbose' => ' %indicator% %message% (%elapsed:6s%)',
'verbose_no_ansi' => ' %message% (%elapsed:6s%)',
'very_verbose' => ' %indicator% %message% (%elapsed:6s%, %memory:6s%)',
'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)',
];
private $output;
private $startTime;
private $format;
@@ -30,14 +41,14 @@ class ProgressIndicator
private $indicatorUpdateTime;
private $started = false;
/**
* @var array<string, callable>
*/
private static $formatters;
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 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)
{
@@ -65,10 +76,8 @@ class ProgressIndicator
/**
* Sets the current indicator message.
*
* @param string|null $message
*/
public function setMessage($message)
public function setMessage(?string $message)
{
$this->message = $message;
@@ -77,10 +86,8 @@ class ProgressIndicator
/**
* Starts the indicator output.
*
* @param $message
*/
public function start($message)
public function start(string $message)
{
if ($this->started) {
throw new LogicException('Progress indicator already started.');
@@ -125,7 +132,7 @@ class ProgressIndicator
*
* @param $message
*/
public function finish($message)
public function finish(string $message)
{
if (!$this->started) {
throw new LogicException('Progress indicator has not yet been started.');
@@ -140,28 +147,19 @@ class ProgressIndicator
/**
* Gets the format for a given name.
*
* @param string $name The format name
*
* @return string|null A format string
* @return string|null
*/
public static function getFormatDefinition($name)
public static function getFormatDefinition(string $name)
{
if (!self::$formats) {
self::$formats = self::initFormats();
}
return isset(self::$formats[$name]) ? self::$formats[$name] : null;
return self::FORMATS[$name] ?? null;
}
/**
* Sets a placeholder formatter for a given name.
*
* This method also allow you to override an existing placeholder.
*
* @param string $name The placeholder name (including the delimiter char like %)
* @param callable $callable A PHP callable
*/
public static function setPlaceholderFormatterDefinition($name, $callable)
public static function setPlaceholderFormatterDefinition(string $name, callable $callable)
{
if (!self::$formatters) {
self::$formatters = self::initPlaceholderFormatters();
@@ -171,19 +169,17 @@ class ProgressIndicator
}
/**
* Gets the placeholder formatter for a given name.
* Gets the placeholder formatter for a given name (including the delimiter char like %).
*
* @param string $name The placeholder name (including the delimiter char like %)
*
* @return callable|null A PHP callable
* @return callable|null
*/
public static function getPlaceholderFormatterDefinition($name)
public static function getPlaceholderFormatterDefinition(string $name)
{
if (!self::$formatters) {
self::$formatters = self::initPlaceholderFormatters();
}
return isset(self::$formatters[$name]) ? self::$formatters[$name] : null;
return self::$formatters[$name] ?? null;
}
private function display()
@@ -192,18 +188,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 +224,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) {
@@ -252,18 +246,4 @@ class ProgressIndicator
},
];
}
private static function initFormats()
{
return [
'normal' => ' %indicator% %message%',
'normal_no_ansi' => ' %message%',
'verbose' => ' %indicator% %message% (%elapsed:6s%)',
'verbose_no_ansi' => ' %message% (%elapsed:6s%)',
'very_verbose' => ' %indicator% %message% (%elapsed:6s%, %memory:6s%)',
'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)',
];
}
}

View File

@@ -11,6 +11,8 @@
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;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
@@ -21,6 +23,8 @@ 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;
use function Symfony\Component\String\s;
/**
* The QuestionHelper class provides helpers to interact with the user.
@@ -29,9 +33,13 @@ use Symfony\Component\Console\Question\Question;
*/
class QuestionHelper extends Helper
{
/**
* @var resource|null
*/
private $inputStream;
private static $shell;
private static $stty;
private static $stty = true;
private static $stdinIsInteractive;
/**
* Asks a question to the user.
@@ -47,44 +55,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 +102,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 +110,15 @@ class QuestionHelper extends Helper
{
$this->writePrompt($output, $question);
$inputStream = $this->inputStream ?: STDIN;
$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 +127,17 @@ class QuestionHelper extends Helper
}
if (false === $ret) {
$ret = fgets($inputStream, 4096);
$ret = $this->readInput($inputStream, $question);
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, $autocomplete));
$autocomplete = $this->autocomplete($output, $question, $inputStream, $autocomplete);
$ret = $question->isTrimmable() ? trim($autocomplete) : $autocomplete;
}
if ($output instanceof ConsoleSectionOutput) {
@@ -153,6 +153,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 +191,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 +201,24 @@ class QuestionHelper extends Helper
$output->write($message);
}
/**
* @return string[]
*/
protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag)
{
$messages = [];
$maxWidth = max(array_map([__CLASS__, 'width'], array_keys($choices = $question->getChoices())));
foreach ($choices as $key => $value) {
$padding = str_repeat(' ', $maxWidth - self::width($key));
$messages[] = sprintf(" [<$tag>%s$padding</$tag>] %s", $key, $value);
}
return $messages;
}
/**
* Outputs an error message.
*/
@@ -198,6 +240,8 @@ class QuestionHelper extends Helper
*/
private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string
{
$cursor = new Cursor($output, $inputStream);
$fullChoice = '';
$ret = '';
@@ -207,6 +251,9 @@ class QuestionHelper extends Helper
$numMatches = \count($matches);
$sttyMode = shell_exec('stty -g');
$isStdin = 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null);
$r = [$inputStream];
$w = [];
// Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
shell_exec('stty -icanon -echo');
@@ -216,18 +263,22 @@ class QuestionHelper extends Helper
// Read a keypress
while (!feof($inputStream)) {
while ($isStdin && 0 === @stream_select($r, $w, $w, 0, 100)) {
// Give signal handlers a chance to run
$r = [$inputStream];
}
$c = fread($inputStream, 1);
// 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.');
shell_exec('stty '.$sttyMode);
throw new MissingInputException('Aborted.');
} elseif ("\177" === $c) { // Backspace Character
if (0 === $numMatches && 0 !== $i) {
--$i;
$fullChoice = substr($fullChoice, 0, -1);
// Move cursor backwards
$output->write("\033[1D");
$cursor->moveLeft(s($fullChoice)->slice(-1)->width(false));
$fullChoice = self::substr($fullChoice, 0, $i);
}
if (0 === $i) {
@@ -239,7 +290,7 @@ class QuestionHelper extends Helper
}
// 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);
@@ -265,12 +316,12 @@ class QuestionHelper extends Helper
$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 || 0 === strpos($match, $ret);
return '' === $ret || str_starts_with($match, $ret);
}
);
$numMatches = \count($matches);
@@ -281,6 +332,8 @@ class QuestionHelper extends Helper
$output->write($c);
break;
}
$numMatches = 0;
}
continue;
@@ -305,41 +358,38 @@ 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();
}
}
// Reset stty so it behaves normally again
shell_exec(sprintf('stty %s', $sttyMode));
shell_exec('stty '.$sttyMode);
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;
}
@@ -349,12 +399,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';
@@ -366,7 +416,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)) {
@@ -376,41 +427,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);
shell_exec(sprintf('stty %s', $sttyMode));
} elseif ($this->isInteractiveInput($inputStream)) {
throw new RuntimeException('Unable to hide the response.');
}
if (false === $value) {
throw new RuntimeException('Aborted.');
}
$value = fgets($inputStream, 4096);
if (self::$stty && Terminal::hasSttyAvailable()) {
shell_exec('stty '.$sttyMode);
}
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
*
@@ -420,6 +464,7 @@ class QuestionHelper extends Helper
{
$error = null;
$attempts = $question->getMaxAttempts();
while (null === $attempts || $attempts--) {
if (null !== $error) {
$this->writeError($output, $error);
@@ -436,44 +481,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;
}
/**
* Returns whether Stty is available or not.
* 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 hasSttyAvailable(): bool
private function cloneInputStream($inputStream)
{
if (null !== self::$stty) {
return self::$stty;
$streamMetaData = stream_get_meta_data($inputStream);
$seekable = $streamMetaData['seekable'] ?? false;
$mode = $streamMetaData['mode'] ?? 'rb';
$uri = $streamMetaData['uri'] ?? null;
if (null === $uri) {
return null;
}
exec('stty 2>&1', $output, $exitcode);
$cloneStream = fopen($uri, $mode);
return self::$stty = 0 === $exitcode;
// 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;
}
}

View File

@@ -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;
@@ -68,15 +72,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);
}
/**
@@ -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>';
}
}

View File

@@ -48,6 +48,7 @@ class Table
* Table rows.
*/
private $rows = [];
private $horizontal = false;
/**
* Column widths cache.
@@ -84,6 +85,9 @@ class Table
private $columnWidths = [];
private $columnMaxWidths = [];
/**
* @var array<string, TableStyle>|null
*/
private static $styles;
private $rendered = false;
@@ -101,11 +105,8 @@ class Table
/**
* Sets a style definition.
*
* @param string $name The style name
* @param TableStyle $style A TableStyle instance
*/
public static function setStyleDefinition($name, TableStyle $style)
public static function setStyleDefinition(string $name, TableStyle $style)
{
if (!self::$styles) {
self::$styles = self::initStyles();
@@ -117,11 +118,9 @@ class Table
/**
* Gets a style definition by name.
*
* @param string $name The style name
*
* @return TableStyle
*/
public static function getStyleDefinition($name)
public static function getStyleDefinition(string $name)
{
if (!self::$styles) {
self::$styles = self::initStyles();
@@ -161,15 +160,12 @@ class Table
/**
* Sets table column style.
*
* @param int $columnIndex Column index
* @param TableStyle|string $name The style name or a TableStyle instance
* @param TableStyle|string $name The style name or a TableStyle instance
*
* @return $this
*/
public function setColumnStyle($columnIndex, $name)
public function setColumnStyle(int $columnIndex, $name)
{
$columnIndex = (int) $columnIndex;
$this->columnStyles[$columnIndex] = $this->resolveStyle($name);
return $this;
@@ -180,11 +176,9 @@ class Table
*
* If style was not set, it returns the global table style.
*
* @param int $columnIndex Column index
*
* @return TableStyle
*/
public function getColumnStyle($columnIndex)
public function getColumnStyle(int $columnIndex)
{
return $this->columnStyles[$columnIndex] ?? $this->getStyle();
}
@@ -192,14 +186,11 @@ class Table
/**
* Sets the minimum width of a column.
*
* @param int $columnIndex Column index
* @param int $width Minimum column width in characters
*
* @return $this
*/
public function setColumnWidth($columnIndex, $width)
public function setColumnWidth(int $columnIndex, int $width)
{
$this->columnWidths[(int) $columnIndex] = (int) $width;
$this->columnWidths[$columnIndex] = $width;
return $this;
}
@@ -207,8 +198,6 @@ class Table
/**
* Sets the minimum width of all columns.
*
* @param array $widths
*
* @return $this
*/
public function setColumnWidths(array $widths)
@@ -232,7 +221,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;
@@ -240,6 +229,9 @@ class Table
return $this;
}
/**
* @return $this
*/
public function setHeaders(array $headers)
{
$headers = array_values($headers);
@@ -259,6 +251,9 @@ class Table
return $this->addRows($rows);
}
/**
* @return $this
*/
public function addRows(array $rows)
{
foreach ($rows as $row) {
@@ -268,6 +263,9 @@ class Table
return $this;
}
/**
* @return $this
*/
public function addRow($row)
{
if ($row instanceof TableSeparator) {
@@ -287,6 +285,8 @@ class Table
/**
* Adds a row to the table, and re-renders the table.
*
* @return $this
*/
public function appendRow($row): self
{
@@ -304,6 +304,9 @@ class Table
return $this;
}
/**
* @return $this
*/
public function setRow($column, array $row)
{
$this->rows[$column] = $row;
@@ -311,6 +314,9 @@ class Table
return $this;
}
/**
* @return $this
*/
public function setHeaderTitle(?string $title): self
{
$this->headerTitle = $title;
@@ -318,6 +324,9 @@ class Table
return $this;
}
/**
* @return $this
*/
public function setFooterTitle(?string $title): self
{
$this->footerTitle = $title;
@@ -325,6 +334,16 @@ class Table
return $this;
}
/**
* @return $this
*/
public function setHorizontal(bool $horizontal = true): self
{
$this->horizontal = $horizontal;
return $this;
}
/**
* Renders table to output.
*
@@ -340,14 +359,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 +406,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 +446,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;
@@ -417,15 +462,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 {
@@ -439,7 +484,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 +498,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 +517,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).
@@ -490,16 +539,39 @@ 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));
}
/**
* Calculate number of columns for this table.
*/
private function calculateNumberOfColumns($rows)
private function calculateNumberOfColumns(array $rows)
{
$columns = [0];
foreach ($rows as $row) {
@@ -513,7 +585,7 @@ class Table
$this->numberOfColumns = max($columns);
}
private function buildTableRows($rows)
private function buildTableRows(array $rows): TableRows
{
/** @var WrappableOutputFormatterInterface $formatter */
$formatter = $this->output->getFormatter();
@@ -525,10 +597,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)));
@@ -541,19 +613,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 +643,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 +660,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;
@@ -592,15 +669,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;
}
@@ -632,9 +709,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) {
@@ -705,7 +783,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) {
@@ -718,13 +796,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
@@ -733,10 +811,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;
@@ -751,7 +829,10 @@ class Table
$this->numberOfColumns = null;
}
private static function initStyles()
/**
* @return array<string, TableStyle>
*/
private static function initStyles(): array
{
$borderless = new TableStyle();
$borderless
@@ -798,7 +879,7 @@ class Table
];
}
private function resolveStyle($name)
private function resolveStyle($name): TableStyle
{
if ($name instanceof TableStyle) {
return $name;

View File

@@ -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'];
}
}

View File

@@ -0,0 +1,89 @@
<?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 const TAG_OPTIONS = [
'fg',
'bg',
'options',
];
private const ALIGN_MAP = [
'left' => \STR_PAD_RIGHT,
'center' => \STR_PAD_BOTH,
'right' => \STR_PAD_LEFT,
];
private $options = [
'fg' => 'default',
'bg' => 'default',
'options' => null,
'align' => self::DEFAULT_ALIGN,
'cellFormat' => null,
];
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'], self::ALIGN_MAP)) {
throw new InvalidArgumentException(sprintf('Wrong align value. Value must be following: \'%s\'.', implode('\', \'', array_keys(self::ALIGN_MAP))));
}
$this->options = array_merge($this->options, $options);
}
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, self::TAG_OPTIONS) && isset($this->options[$key]);
},
\ARRAY_FILTER_USE_KEY
);
}
/**
* @return int
*/
public function getPadByAlign()
{
return self::ALIGN_MAP[$this->getOptions()['align']];
}
public function getCellFormat(): ?string
{
return $this->getOptions()['cellFormat'];
}
}

View File

@@ -18,15 +18,13 @@ class TableRows implements \IteratorAggregate
{
private $generator;
public function __construct(callable $generator)
public function __construct(\Closure $generator)
{
$this->generator = $generator;
}
public function getIterator()
public function getIterator(): \Traversable
{
$g = $this->generator;
return $g();
return ($this->generator)();
}
}

View File

@@ -46,19 +46,17 @@ 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.
*
* @param string $paddingChar
*
* @return $this
*/
public function setPaddingChar($paddingChar)
public function setPaddingChar(string $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;
@@ -90,8 +88,7 @@ class TableStyle
* ╚═══════════════╧══════════════════════════╧══════════════════╝
* </code>
*
* @param string $outside Outside border char (see #1 of example)
* @param string|null $inside Inside border char (see #2 of example), equals $outside if null
* @return $this
*/
public function setHorizontalBorderChars(string $outside, string $inside = null): self
{
@@ -101,36 +98,6 @@ class TableStyle
return $this;
}
/**
* Sets horizontal border character.
*
* @param string $horizontalBorderChar
*
* @return $this
*
* @deprecated since Symfony 4.1, use {@link setHorizontalBorderChars()} instead.
*/
public function setHorizontalBorderChar($horizontalBorderChar)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use setHorizontalBorderChars() instead.', __METHOD__), E_USER_DEPRECATED);
return $this->setHorizontalBorderChars($horizontalBorderChar, $horizontalBorderChar);
}
/**
* Gets horizontal border character.
*
* @return string
*
* @deprecated since Symfony 4.1, use {@link getBorderChars()} instead.
*/
public function getHorizontalBorderChar()
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use getBorderChars() instead.', __METHOD__), E_USER_DEPRECATED);
return $this->horizontalOutsideBorderChar;
}
/**
* Sets vertical border characters.
*
@@ -146,8 +113,7 @@ class TableStyle
* ╚═══════════════╧══════════════════════════╧══════════════════╝
* </code>
*
* @param string $outside Outside border char (see #1 of example)
* @param string|null $inside Inside border char (see #2 of example), equals $outside if null
* @return $this
*/
public function setVerticalBorderChars(string $outside, string $inside = null): self
{
@@ -157,42 +123,12 @@ class TableStyle
return $this;
}
/**
* Sets vertical border character.
*
* @param string $verticalBorderChar
*
* @return $this
*
* @deprecated since Symfony 4.1, use {@link setVerticalBorderChars()} instead.
*/
public function setVerticalBorderChar($verticalBorderChar)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use setVerticalBorderChars() instead.', __METHOD__), E_USER_DEPRECATED);
return $this->setVerticalBorderChars($verticalBorderChar, $verticalBorderChar);
}
/**
* Gets vertical border character.
*
* @return string
*
* @deprecated since Symfony 4.1, use {@link getBorderChars()} instead.
*/
public function getVerticalBorderChar()
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use getBorderChars() instead.', __METHOD__), E_USER_DEPRECATED);
return $this->verticalOutsideBorderChar;
}
/**
* Gets border characters.
*
* @internal
*/
public function getBorderChars()
public function getBorderChars(): array
{
return [
$this->horizontalOutsideBorderChar,
@@ -230,6 +166,8 @@ class TableStyle
* @param string|null $topLeftBottom Top left bottom char (see #8' of example), equals to $midLeft if null
* @param string|null $topMidBottom Top mid bottom char (see #0' of example), equals to $cross if null
* @param string|null $topRightBottom Top right bottom char (see #4' of example), equals to $midRight if null
*
* @return $this
*/
public function setCrossingChars(string $cross, string $topLeft, string $topMid, string $topRight, string $midRight, string $bottomRight, string $bottomMid, string $bottomLeft, string $midLeft, string $topLeftBottom = null, string $topMidBottom = null, string $topRightBottom = null): self
{
@@ -259,22 +197,6 @@ class TableStyle
return $this->setCrossingChars($char, $char, $char, $char, $char, $char, $char, $char, $char);
}
/**
* Sets crossing character.
*
* @param string $crossingChar
*
* @return $this
*
* @deprecated since Symfony 4.1. Use {@link setDefaultCrossingChar()} instead.
*/
public function setCrossingChar($crossingChar)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1. Use setDefaultCrossingChar() instead.', __METHOD__), E_USER_DEPRECATED);
return $this->setDefaultCrossingChar($crossingChar);
}
/**
* Gets crossing character.
*
@@ -311,11 +233,9 @@ class TableStyle
/**
* Sets header cell format.
*
* @param string $cellHeaderFormat
*
* @return $this
*/
public function setCellHeaderFormat($cellHeaderFormat)
public function setCellHeaderFormat(string $cellHeaderFormat)
{
$this->cellHeaderFormat = $cellHeaderFormat;
@@ -335,11 +255,9 @@ class TableStyle
/**
* Sets row cell format.
*
* @param string $cellRowFormat
*
* @return $this
*/
public function setCellRowFormat($cellRowFormat)
public function setCellRowFormat(string $cellRowFormat)
{
$this->cellRowFormat = $cellRowFormat;
@@ -359,11 +277,9 @@ class TableStyle
/**
* Sets row cell content format.
*
* @param string $cellRowContentFormat
*
* @return $this
*/
public function setCellRowContentFormat($cellRowContentFormat)
public function setCellRowContentFormat(string $cellRowContentFormat)
{
$this->cellRowContentFormat = $cellRowContentFormat;
@@ -383,11 +299,9 @@ class TableStyle
/**
* Sets table border format.
*
* @param string $borderFormat
*
* @return $this
*/
public function setBorderFormat($borderFormat)
public function setBorderFormat(string $borderFormat)
{
$this->borderFormat = $borderFormat;
@@ -407,13 +321,11 @@ class TableStyle
/**
* Sets cell padding type.
*
* @param int $padType STR_PAD_*
*
* @return $this
*/
public function setPadType($padType)
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).');
}
@@ -437,6 +349,9 @@ class TableStyle
return $this->headerTitleFormat;
}
/**
* @return $this
*/
public function setHeaderTitleFormat(string $format): self
{
$this->headerTitleFormat = $format;
@@ -449,6 +364,9 @@ class TableStyle
return $this->footerTitleFormat;
}
/**
* @return $this
*/
public function setFooterTitleFormat(string $format): self
{
$this->footerTitleFormat = $format;