From 4e76aa3fbcc2e74fbe9c9cab441d20a0ec871f09 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 1 Mar 2014 05:11:27 +0100 Subject: [PATCH 1/9] [Console] added ProgressBar (to replace the stateful ProgressHelper class) --- UPGRADE-3.0.md | 25 ++ src/Symfony/Component/Console/CHANGELOG.md | 5 +- .../Component/Console/Helper/Helper.php | 2 +- .../Component/Console/Helper/ProgressBar.php | 403 ++++++++++++++++++ .../Console/Helper/ProgressHelper.php | 2 + .../Console/Tests/Helper/ProgressBarTest.php | 318 ++++++++++++++ 6 files changed, 752 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Component/Console/Helper/ProgressBar.php create mode 100644 src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index a87d61d48ae0a..afb0600602d2f 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -23,6 +23,31 @@ UPGRADE FROM 2.x to 3.0 * The methods `isQuiet`, `isVerbose`, `isVeryVerbose` and `isDebug` were added to `Symfony\Component\Console\Output\OutputInterface`. + * `ProgressHelper` has been removed in favor of `ProgressBar`. + + Before: + + ``` + $h = new ProgressHelper(); + $h->start($output, 10); + for ($i = 1; $i < 5; $i++) { + usleep(200000); + $h->advance(); + } + $h->finish(); + ``` + + After: + + ``` + $bar = new ProgressBar($output, 10); + $bar->start(); + for ($i = 1; $i < 5; $i++) { + usleep(200000); + $bar->advance(); + } + ``` + ### EventDispatcher * The interface `Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcherInterface` diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 19d03ca3039ea..00bb1303838ff 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -4,8 +4,9 @@ CHANGELOG 2.5.0 ----- -* added a way to set a default command instead of `ListCommand` -* added a way to set the process name of a command + * deprecated ProgressHelper in favor of ProgressBar + * added a way to set a default command instead of `ListCommand` + * added a way to set the process name of a command 2.4.0 ----- diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index 534b9f4319737..63d1be8c1e52e 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -47,7 +47,7 @@ public function getHelperSet() * * @return integer The length of the string */ - protected function strlen($string) + public static function strlen($string) { if (!function_exists('mb_strlen')) { return strlen($string); diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php new file mode 100644 index 0000000000000..0d9667fd906e0 --- /dev/null +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -0,0 +1,403 @@ + + * + * 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\NullOutput; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * The ProgressBar provides helpers to display progress output. + * + * @author Fabien Potencier + * @author Chris Jones + */ +class ProgressBar +{ + const FORMAT_QUIET = ' %percent%%'; + const FORMAT_NORMAL = ' %current%/%max% [%bar%] %percent%%'; + const FORMAT_VERBOSE = ' %current%/%max% [%bar%] %percent%% Elapsed: %elapsed%'; + const FORMAT_QUIET_NOMAX = ' %current%'; + const FORMAT_NORMAL_NOMAX = ' %current% [%bar%]'; + const FORMAT_VERBOSE_NOMAX = ' %current% [%bar%] Elapsed: %elapsed%'; + + // options + private $barWidth = 28; + private $barChar = '='; + private $emptyBarChar = '-'; + private $progressChar = '>'; + private $format = null; + private $redrawFreq = 1; + + /** + * @var OutputInterface + */ + private $output; + private $step; + private $max; + private $startTime; + private $lastMessagesLength; + private $barCharOriginal; + + /** + * List of formatting variables + * + * @var array + */ + private $defaultFormatVars = array( + 'current', + 'max', + 'bar', + 'percent', + 'elapsed', + ); + + /** + * Available formatting variables + * + * @var array + */ + private $formatVars; + + /** + * Various time formats + * + * @var array + */ + private $timeFormats = array( + array(0, '???'), + array(2, '1 sec'), + array(59, 'secs', 1), + array(60, '1 min'), + array(3600, 'mins', 60), + array(5400, '1 hr'), + array(86400, 'hrs', 3600), + array(129600, '1 day'), + array(604800, 'days', 86400), + ); + + private $stepWidth; + private $percent; + + /** + * Constructor. + * + * @param OutputInterface $output An OutputInterface instance + * @param integer $max Maximum steps (0 if unknown) + */ + public function __construct(OutputInterface $output, $max = 0) + { + // Disabling output when it does not support ANSI codes as it would result in a broken display anyway. + $this->output = $output->isDecorated() ? $output : new NullOutput(); + $this->max = (int) $max; + $this->stepWidth = $this->max > 0 ? Helper::strlen($this->max) : 4; + } + + /** + * Sets the progress bar width. + * + * @param int $size The progress bar size + */ + public function setBarWidth($size) + { + $this->barWidth = (int) $size; + } + + /** + * Sets the bar character. + * + * @param string $char A character + */ + public function setBarCharacter($char) + { + $this->barChar = $char; + } + + /** + * Sets the empty bar character. + * + * @param string $char A character + */ + public function setEmptyBarCharacter($char) + { + $this->emptyBarChar = $char; + } + + /** + * Sets the progress bar character. + * + * @param string $char A character + */ + public function setProgressCharacter($char) + { + $this->progressChar = $char; + } + + /** + * Sets the progress bar format. + * + * @param string $format The format + */ + public function setFormat($format) + { + $this->format = $format; + } + + /** + * Sets the redraw frequency. + * + * @param int $freq The frequency in steps + */ + public function setRedrawFrequency($freq) + { + $this->redrawFreq = (int) $freq; + } + + /** + * Starts the progress output. + */ + public function start() + { + $this->startTime = time(); + $this->step = 0; + $this->percent = 0; + $this->lastMessagesLength = 0; + $this->barCharOriginal = ''; + + if (null === $this->format) { + $this->format = $this->determineBestFormat(); + } + + $this->formatVars = array(); + foreach ($this->defaultFormatVars as $var) { + if (false !== strpos($this->format, "%{$var}%")) { + $this->formatVars[$var] = true; + } + } + + if (!$this->max) { + $this->barCharOriginal = $this->barChar; + $this->barChar = $this->emptyBarChar; + } + + $this->display(); + } + + /** + * Advances the progress output X steps. + * + * @param integer $step Number of steps to advance + * + * @throws \LogicException + */ + public function advance($step = 1) + { + $this->setCurrent($this->step + $step); + } + + /** + * Sets the current progress. + * + * @param integer $step The current progress + * + * @throws \LogicException + */ + public function setCurrent($step) + { + if (null === $this->startTime) { + throw new \LogicException('You must start the progress bar before calling setCurrent().'); + } + + $step = (int) $step; + if ($step < $this->step) { + throw new \LogicException('You can\'t regress the progress bar.'); + } + + if ($this->max > 0 && $step > $this->max) { + throw new \LogicException('You can\'t advance the progress bar past the max value.'); + } + + $prevPeriod = intval($this->step / $this->redrawFreq); + $currPeriod = intval($step / $this->redrawFreq); + $this->step = $step; + $this->percent = $this->max > 0 ? (float) $this->step / $this->max : 0; + if ($prevPeriod !== $currPeriod || $this->max === $step) { + $this->display(); + } + } + + /** + * Finishes the progress output. + */ + public function finish() + { + if (null === $this->startTime) { + throw new \LogicException('You must start the progress bar before calling finish().'); + } + + if (!$this->max) { + $this->barChar = $this->barCharOriginal; + $this->max = $this->step; + $this->setCurrent($this->max); + $this->max = 0; + $this->barChar = $this->emptyBarChar; + } else { + $this->setCurrent($this->max); + } + + $this->startTime = null; + } + + /** + * Outputs the current progress string. + * + * @throws \LogicException + */ + public function display() + { + if (null === $this->startTime) { + throw new \LogicException('You must start the progress bar before calling display().'); + } + + $message = $this->format; + foreach ($this->generate() as $name => $value) { + $message = str_replace("%{$name}%", $value, $message); + } + $this->overwrite($message); + } + + /** + * Removes the progress bar from the current line. + * + * This is useful if you wish to write some output + * while a progress bar is running. + * Call display() to show the progress bar again. + */ + public function clear() + { + $this->overwrite(''); + } + + /** + * Generates the array map of format variables to values. + * + * @return array Array of format vars and values + */ + private function generate() + { + $vars = array(); + + if (isset($this->formatVars['bar'])) { + $completeBars = floor($this->max > 0 ? $this->percent * $this->barWidth : $this->step % $this->barWidth); + $emptyBars = $this->barWidth - $completeBars - Helper::strlen($this->progressChar); + $bar = str_repeat($this->barChar, $completeBars); + if ($completeBars < $this->barWidth) { + $bar .= $this->progressChar; + $bar .= str_repeat($this->emptyBarChar, $emptyBars); + } + + $vars['bar'] = $bar; + } + + if (isset($this->formatVars['elapsed'])) { + $elapsed = time() - $this->startTime; + $vars['elapsed'] = str_pad($this->humaneTime($elapsed), 6, ' ', STR_PAD_LEFT); + } + + if (isset($this->formatVars['current'])) { + $vars['current'] = str_pad($this->step, $this->stepWidth, ' ', STR_PAD_LEFT); + } + + if (isset($this->formatVars['max'])) { + $vars['max'] = $this->max; + } + + if (isset($this->formatVars['percent'])) { + $vars['percent'] = str_pad(floor($this->percent * 100), 3, ' ', STR_PAD_LEFT); + } + + return $vars; + } + + /** + * Converts seconds into human-readable format. + * + * @param integer $secs Number of seconds + * + * @return string Time in readable format + */ + private function humaneTime($secs) + { + $text = ''; + foreach ($this->timeFormats as $format) { + if ($secs < $format[0]) { + if (count($format) == 2) { + $text = $format[1]; + break; + } else { + $text = ceil($secs / $format[2]).' '.$format[1]; + break; + } + } + } + + return $text; + } + + /** + * Overwrites a previous message to the output. + * + * @param string $message The message + */ + private function overwrite($message) + { + $length = Helper::strlen($message); + + // append whitespace to match the last line's length + if (null !== $this->lastMessagesLength && $this->lastMessagesLength > $length) { + $message = str_pad($message, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT); + } + + // carriage return + $this->output->write("\x0D"); + $this->output->write($message); + + $this->lastMessagesLength = Helper::strlen($message); + } + + private function determineBestFormat() + { + switch ($this->output->getVerbosity()) { + case OutputInterface::VERBOSITY_QUIET: + $format = self::FORMAT_QUIET_NOMAX; + if ($this->max > 0) { + $format = self::FORMAT_QUIET; + } + break; + case OutputInterface::VERBOSITY_VERBOSE: + case OutputInterface::VERBOSITY_VERY_VERBOSE: + case OutputInterface::VERBOSITY_DEBUG: + $format = self::FORMAT_VERBOSE_NOMAX; + if ($this->max > 0) { + $format = self::FORMAT_VERBOSE; + } + break; + default: + $format = self::FORMAT_NORMAL_NOMAX; + if ($this->max > 0) { + $format = self::FORMAT_NORMAL; + } + break; + } + + return $format; + } +} diff --git a/src/Symfony/Component/Console/Helper/ProgressHelper.php b/src/Symfony/Component/Console/Helper/ProgressHelper.php index 7a400874bc03c..eeae23afcbaaa 100644 --- a/src/Symfony/Component/Console/Helper/ProgressHelper.php +++ b/src/Symfony/Component/Console/Helper/ProgressHelper.php @@ -19,6 +19,8 @@ * * @author Chris Jones * @author Fabien Potencier + * + * @deprecated Deprecated since 2.5, to be removed in 3.0; use ProgressBar instead. */ class ProgressHelper extends Helper { diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php new file mode 100644 index 0000000000000..a9fa72c3b24bc --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -0,0 +1,318 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Helper; + +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Output\StreamOutput; + +class ProgressBarTest extends \PHPUnit_Framework_TestCase +{ + protected $lastMessagesLength; + + public function testAdvance() + { + $bar = new ProgressBar($output = $this->getOutputStream()); + $bar->start(); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0 [>---------------------------]'). + $this->generateOutput(' 1 [->--------------------------]'), + stream_get_contents($output->getStream()) + ); + } + + public function testAdvanceWithStep() + { + $bar = new ProgressBar($output = $this->getOutputStream()); + $bar->start(); + $bar->advance(5); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0 [>---------------------------]'). + $this->generateOutput(' 5 [----->----------------------]'), + stream_get_contents($output->getStream()) + ); + } + + public function testAdvanceMultipleTimes() + { + $bar = new ProgressBar($output = $this->getOutputStream()); + $bar->start(); + $bar->advance(3); + $bar->advance(2); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0 [>---------------------------]'). + $this->generateOutput(' 3 [--->------------------------]'). + $this->generateOutput(' 5 [----->----------------------]'), + stream_get_contents($output->getStream()) + ); + } + + public function testCustomizations() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 10); + $bar->setBarWidth(10); + $bar->setBarCharacter('_'); + $bar->setEmptyBarCharacter(' '); + $bar->setProgressCharacter('/'); + $bar->setFormat(' %current%/%max% [%bar%] %percent%%'); + $bar->start(); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/10 [/ ] 0%'). + $this->generateOutput(' 1/10 [_/ ] 10%'), + stream_get_contents($output->getStream()) + ); + } + + public function testPercent() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 50); + $bar->start(); + $bar->display(); + $bar->advance(); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 1/50 [>---------------------------] 2%'). + $this->generateOutput(' 2/50 [=>--------------------------] 4%'), + stream_get_contents($output->getStream()) + ); + } + + public function testOverwriteWithShorterLine() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 50); + $bar->setFormat(' %current%/%max% [%bar%] %percent%%'); + $bar->start(); + $bar->display(); + $bar->advance(); + + // set shorter format + $bar->setFormat(' %current%/%max% [%bar%]'); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 1/50 [>---------------------------] 2%'). + $this->generateOutput(' 2/50 [=>--------------------------] '), + stream_get_contents($output->getStream()) + ); + } + + public function testSetCurrentProgress() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 50); + $bar->start(); + $bar->display(); + $bar->advance(); + $bar->setCurrent(15); + $bar->setCurrent(25); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 1/50 [>---------------------------] 2%'). + $this->generateOutput(' 15/50 [========>-------------------] 30%'). + $this->generateOutput(' 25/50 [==============>-------------] 50%'), + stream_get_contents($output->getStream()) + ); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage You must start the progress bar + */ + public function testSetCurrentBeforeStarting() + { + $bar = new ProgressBar($this->getOutputStream()); + $bar->setCurrent(15); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage You can't regress the progress bar + */ + public function testRegressProgress() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 50); + $bar->start(); + $bar->setCurrent(15); + $bar->setCurrent(10); + } + + public function testRedrawFrequency() + { + $bar = $this->getMock('Symfony\Component\Console\Helper\ProgressBar', array('display'), array($output = $this->getOutputStream(), 6)); + $bar->expects($this->exactly(4))->method('display'); + + $bar->setRedrawFrequency(2); + $bar->start(); + $bar->setCurrent(1); + $bar->advance(2); + $bar->advance(2); + $bar->advance(1); + } + + public function testMultiByteSupport() + { + if (!function_exists('mb_strlen') || (false === $encoding = mb_detect_encoding('■'))) { + $this->markTestSkipped('The mbstring extension is needed for multi-byte support'); + } + + $bar = new ProgressBar($output = $this->getOutputStream()); + $bar->start(); + $bar->setBarCharacter('■'); + $bar->advance(3); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0 [>---------------------------]'). + $this->generateOutput(' 3 [■■■>------------------------]'), + stream_get_contents($output->getStream()) + ); + } + + public function testClear() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 50); + $bar->start(); + $bar->setCurrent(25); + $bar->clear(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/50 [>---------------------------] 0%'). + $this->generateOutput(' 25/50 [==============>-------------] 50%'). + $this->generateOutput(''), + stream_get_contents($output->getStream()) + ); + } + + public function testPercentNotHundredBeforeComplete() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 200); + $bar->start(); + $bar->display(); + $bar->advance(199); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/200 [>---------------------------] 0%'). + $this->generateOutput(' 0/200 [>---------------------------] 0%'). + $this->generateOutput(' 199/200 [===========================>] 99%'). + $this->generateOutput(' 200/200 [============================] 100%'), + stream_get_contents($output->getStream()) + ); + } + + public function testNonDecoratedOutput() + { + $bar = new ProgressBar($output = $this->getOutputStream(false)); + $bar->start(); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals('', stream_get_contents($output->getStream())); + } + + public function testParallelBars() + { + $output = $this->getOutputStream(); + $bar1 = new ProgressBar($output, 2); + $bar2 = new ProgressBar($output, 3); + $bar2->setProgressCharacter('#'); + $bar3 = new ProgressBar($output); + + $bar1->start(); + $output->write("\n"); + $bar2->start(); + $output->write("\n"); + $bar3->start(); + + for ($i = 1; $i <= 3; $i++) { + // up two lines + $output->write("\033[2A"); + if ($i <= 2) { + $bar1->advance(); + } + $output->write("\n"); + $bar2->advance(); + $output->write("\n"); + $bar3->advance(); + } + $output->write("\033[2A"); + $output->write("\n"); + $output->write("\n"); + $bar3->finish(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/2 [>---------------------------] 0%')."\n". + $this->generateOutput(' 0/3 [#---------------------------] 0%')."\n". + rtrim($this->generateOutput(' 0 [>---------------------------]')). + + "\033[2A". + $this->generateOutput(' 1/2 [==============>-------------] 50%')."\n". + $this->generateOutput(' 1/3 [=========#------------------] 33%')."\n". + rtrim($this->generateOutput(' 1 [->--------------------------]')). + + "\033[2A". + $this->generateOutput(' 2/2 [============================] 100%')."\n". + $this->generateOutput(' 2/3 [==================#---------] 66%')."\n". + rtrim($this->generateOutput(' 2 [-->-------------------------]')). + + "\033[2A". + "\n". + $this->generateOutput(' 3/3 [============================] 100%')."\n". + rtrim($this->generateOutput(' 3 [--->------------------------]')). + + "\033[2A". + "\n". + "\n". + rtrim($this->generateOutput(' 3 [============================]')), + stream_get_contents($output->getStream()) + ); + } + + protected function getOutputStream($decorated = true) + { + return new StreamOutput(fopen('php://memory', 'r+', false), StreamOutput::VERBOSITY_NORMAL, $decorated); + } + + protected function generateOutput($expected) + { + $expectedout = $expected; + + if (null !== $this->lastMessagesLength) { + $expectedout = str_pad($expected, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT); + } + + $this->lastMessagesLength = strlen($expectedout); + + return "\x0D".$expectedout; + } +} From 2a78a09f1b3c68ba734e1edeb68fa93a6c474e15 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 1 Mar 2014 08:26:09 +0100 Subject: [PATCH 2/9] [Console] refactored the progress bar to allow placeholder to be extensible --- .../Component/Console/Helper/Helper.php | 27 ++ .../Component/Console/Helper/ProgressBar.php | 256 ++++++++++-------- .../Console/Tests/Helper/ProgressBarTest.php | 21 ++ 3 files changed, 191 insertions(+), 113 deletions(-) diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index 63d1be8c1e52e..b2882663ee904 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -59,4 +59,31 @@ public static function strlen($string) return mb_strlen($string, $encoding); } + + public static function formatTime($secs) + { + static $timeFormats = array( + array(0, '< 1 sec'), + array(2, '1 sec'), + array(59, 'secs', 1), + array(60, '1 min'), + array(3600, 'mins', 60), + array(5400, '1 hr'), + array(86400, 'hrs', 3600), + array(129600, '1 day'), + array(604800, 'days', 86400), + ); + + foreach ($timeFormats as $format) { + if ($secs >= $format[0]) { + continue; + } + + if (2 == count($format)) { + return $format[1]; + } + + return ceil($secs / $format[2]).' '.$format[1]; + } + } } diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 0d9667fd906e0..3c5e996c88ebb 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -44,61 +44,96 @@ class ProgressBar private $step; private $max; private $startTime; + private $stepWidth; + private $percent; private $lastMessagesLength; private $barCharOriginal; + static private $formatters; + /** - * List of formatting variables + * Constructor. * - * @var array + * @param OutputInterface $output An OutputInterface instance + * @param integer $max Maximum steps (0 if unknown) */ - private $defaultFormatVars = array( - 'current', - 'max', - 'bar', - 'percent', - 'elapsed', - ); + public function __construct(OutputInterface $output, $max = 0) + { + // Disabling output when it does not support ANSI codes as it would result in a broken display anyway. + $this->output = $output->isDecorated() ? $output : new NullOutput(); + $this->max = (int) $max; + $this->stepWidth = $this->max > 0 ? Helper::strlen($this->max) : 4; + + if (!self::$formatters) { + self::$formatters = self::initPlaceholderFormatters(); + } + } /** - * Available formatting variables + * Sets a placeholder formatter for a given name. + * + * This method also allow you to override an existing placeholder. * - * @var array + * @param string $name The placeholder name (including the delimiter char like %) + * @param callable $callable A PHP callable */ - private $formatVars; + public static function setPlaceholderFormatter($name, $callable) + { + if (!self::$formatters) { + self::$formatters = self::initPlaceholderFormatters(); + } + + self::$formatters[$name] = $callable; + } /** - * Various time formats + * Gets the progress bar start time. * - * @var array + * @return int The progress bar start time */ - private $timeFormats = array( - array(0, '???'), - array(2, '1 sec'), - array(59, 'secs', 1), - array(60, '1 min'), - array(3600, 'mins', 60), - array(5400, '1 hr'), - array(86400, 'hrs', 3600), - array(129600, '1 day'), - array(604800, 'days', 86400), - ); + public function getStartTime() + { + return $this->startTime; + } - private $stepWidth; - private $percent; + /** + * Gets the progress bar maximal steps. + * + * @return int The progress bar max steps + */ + public function getMaxSteps() + { + return $this->max; + } /** - * Constructor. + * Gets the progress bar step. * - * @param OutputInterface $output An OutputInterface instance - * @param integer $max Maximum steps (0 if unknown) + * @return int The progress bar step */ - public function __construct(OutputInterface $output, $max = 0) + public function getStep() { - // Disabling output when it does not support ANSI codes as it would result in a broken display anyway. - $this->output = $output->isDecorated() ? $output : new NullOutput(); - $this->max = (int) $max; - $this->stepWidth = $this->max > 0 ? Helper::strlen($this->max) : 4; + return $this->step; + } + + /** + * Gets the progress bar step width. + * + * @return int The progress bar step width + */ + public function getStepWidth() + { + return $this->stepWidth; + } + + /** + * Gets the current progress bar percent. + * + * @return int The current progress bar percent + */ + public function getProgressPercent() + { + return $this->percent; } /** @@ -111,6 +146,16 @@ public function setBarWidth($size) $this->barWidth = (int) $size; } + /** + * Gets the progress bar width. + * + * @return int The progress bar size + */ + public function getBarWidth() + { + return $this->barWidth; + } + /** * Sets the bar character. * @@ -121,6 +166,16 @@ public function setBarCharacter($char) $this->barChar = $char; } + /** + * Gets the bar character. + * + * @return string A character + */ + public function getBarCharacter() + { + return $this->barChar; + } + /** * Sets the empty bar character. * @@ -131,6 +186,16 @@ public function setEmptyBarCharacter($char) $this->emptyBarChar = $char; } + /** + * Gets the empty bar character. + * + * @return string A character + */ + public function getEmptyBarCharacter() + { + return $this->emptyBarChar; + } + /** * Sets the progress bar character. * @@ -141,6 +206,16 @@ public function setProgressCharacter($char) $this->progressChar = $char; } + /** + * Gets the progress bar character. + * + * @return string A character + */ + public function getProgressCharacter() + { + return $this->progressChar; + } + /** * Sets the progress bar format. * @@ -176,13 +251,6 @@ public function start() $this->format = $this->determineBestFormat(); } - $this->formatVars = array(); - foreach ($this->defaultFormatVars as $var) { - if (false !== strpos($this->format, "%{$var}%")) { - $this->formatVars[$var] = true; - } - } - if (!$this->max) { $this->barCharOriginal = $this->barChar; $this->barChar = $this->emptyBarChar; @@ -267,11 +335,11 @@ public function display() throw new \LogicException('You must start the progress bar before calling display().'); } - $message = $this->format; - foreach ($this->generate() as $name => $value) { - $message = str_replace("%{$name}%", $value, $message); - } - $this->overwrite($message); + $regex = implode('|', array_keys(self::$formatters)); + $self = $this; + $this->overwrite(preg_replace_callback("{($regex)}", function ($matches) use ($self) { + return call_user_func(self::$formatters[$matches[1]], $self); + }, $this->format)); } /** @@ -286,72 +354,6 @@ public function clear() $this->overwrite(''); } - /** - * Generates the array map of format variables to values. - * - * @return array Array of format vars and values - */ - private function generate() - { - $vars = array(); - - if (isset($this->formatVars['bar'])) { - $completeBars = floor($this->max > 0 ? $this->percent * $this->barWidth : $this->step % $this->barWidth); - $emptyBars = $this->barWidth - $completeBars - Helper::strlen($this->progressChar); - $bar = str_repeat($this->barChar, $completeBars); - if ($completeBars < $this->barWidth) { - $bar .= $this->progressChar; - $bar .= str_repeat($this->emptyBarChar, $emptyBars); - } - - $vars['bar'] = $bar; - } - - if (isset($this->formatVars['elapsed'])) { - $elapsed = time() - $this->startTime; - $vars['elapsed'] = str_pad($this->humaneTime($elapsed), 6, ' ', STR_PAD_LEFT); - } - - if (isset($this->formatVars['current'])) { - $vars['current'] = str_pad($this->step, $this->stepWidth, ' ', STR_PAD_LEFT); - } - - if (isset($this->formatVars['max'])) { - $vars['max'] = $this->max; - } - - if (isset($this->formatVars['percent'])) { - $vars['percent'] = str_pad(floor($this->percent * 100), 3, ' ', STR_PAD_LEFT); - } - - return $vars; - } - - /** - * Converts seconds into human-readable format. - * - * @param integer $secs Number of seconds - * - * @return string Time in readable format - */ - private function humaneTime($secs) - { - $text = ''; - foreach ($this->timeFormats as $format) { - if ($secs < $format[0]) { - if (count($format) == 2) { - $text = $format[1]; - break; - } else { - $text = ceil($secs / $format[2]).' '.$format[1]; - break; - } - } - } - - return $text; - } - /** * Overwrites a previous message to the output. * @@ -400,4 +402,32 @@ private function determineBestFormat() return $format; } + + static private function initPlaceholderFormatters() + { + return array( + '%bar%' => function (ProgressBar $bar) { + $completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getStep() % $bar->getBarWidth()); + $emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlen($bar->getProgressCharacter()); + $display = str_repeat($bar->getBarCharacter(), $completeBars); + if ($completeBars < $bar->getBarWidth()) { + $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars); + } + + return $display; + }, + '%elapsed%' => function (ProgressBar $bar) { + return str_pad(Helper::formatTime(time() - $bar->getStartTime()), 6, ' ', STR_PAD_LEFT); + }, + '%current%' => function (ProgressBar $bar) { + return str_pad($bar->getStep(), $bar->getStepWidth(), ' ', STR_PAD_LEFT); + }, + '%max%' => function (ProgressBar $bar) { + return $bar->getMaxSteps(); + }, + '%percent%' => function (ProgressBar $bar) { + return str_pad(floor($bar->getProgressPercent() * 100), 3, ' ', STR_PAD_LEFT); + }, + ); + } } diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index a9fa72c3b24bc..73a2ae0dd2de0 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -298,6 +298,27 @@ public function testParallelBars() ); } + public function testAddingPlaceholderFormatter() + { + ProgressBar::setPlaceholderFormatter('%remaining_steps%', function (ProgressBar $bar) { + return $bar->getMaxSteps() - $bar->getStep(); + }); + $bar = new ProgressBar($output = $this->getOutputStream(), 3); + $bar->setFormat(' %remaining_steps% [%bar%]'); + + $bar->start(); + $bar->advance(); + $bar->finish(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 3 [>---------------------------]'). + $this->generateOutput(' 2 [=========>------------------]'). + $this->generateOutput(' 0 [============================]'), + stream_get_contents($output->getStream()) + ); + } + protected function getOutputStream($decorated = true) { return new StreamOutput(fopen('php://memory', 'r+', false), StreamOutput::VERBOSITY_NORMAL, $decorated); From 1aa7b8c8b9e0fdf89e4c76d45cf69e709d394134 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 1 Mar 2014 08:26:32 +0100 Subject: [PATCH 3/9] [Console] added more default placeholder formatters for the progress bar --- .../Component/Console/Helper/Helper.php | 17 +++++++++++ .../Component/Console/Helper/ProgressBar.php | 29 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index b2882663ee904..cdeca3523a68d 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -86,4 +86,21 @@ public static function formatTime($secs) return ceil($secs / $format[2]).' '.$format[1]; } } + + public static function formatMemory($memory) + { + if ($memory >= 1024 * 1024 * 1024) { + return sprintf('%.1f GB', $memory / 1024 / 1024 / 1024); + } + + if ($memory >= 1024 * 1024) { + return sprintf('%.1f MB', $memory / 1024 / 1024); + } + + if ($memory >= 1024) { + return sprintf('%d kB', $memory / 1024); + } + + return sprintf('%d B', $memory); + } } diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 3c5e996c88ebb..b96fbfa86bbbd 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -419,6 +419,35 @@ static private function initPlaceholderFormatters() '%elapsed%' => function (ProgressBar $bar) { return str_pad(Helper::formatTime(time() - $bar->getStartTime()), 6, ' ', STR_PAD_LEFT); }, + '%remaining%' => function (ProgressBar $bar) { + if (!$bar->getMaxSteps()) { + throw new \LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); + } + + if (!$bar->getStep()) { + $remaining = 0; + } else { + $remaining = round((time() - $bar->getStartTime()) / $bar->getStep() * ($bar->getMaxSteps() - $bar->getStep())); + } + + return str_pad(Helper::formatTime($remaining), 6, ' ', STR_PAD_LEFT); + }, + '%estimated%' => function (ProgressBar $bar) { + if (!$bar->getMaxSteps()) { + throw new \LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); + } + + if (!$bar->getStep()) { + $estimated = 0; + } else { + $estimated = round((time() - $bar->getStartTime()) / $bar->getStep() * $bar->getMaxSteps()); + } + + return str_pad(Helper::formatTime($estimated), 6, ' ', STR_PAD_LEFT); + }, + '%memory%' => function (ProgressBar $bar) { + return str_pad(Helper::formatMemory(memory_get_usage(true)), 6, ' ', STR_PAD_LEFT);; + }, '%current%' => function (ProgressBar $bar) { return str_pad($bar->getStep(), $bar->getStepWidth(), ' ', STR_PAD_LEFT); }, From 7a30e50eee8a2fce672bda47884f9b32e09f12c8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 1 Mar 2014 08:54:45 +0100 Subject: [PATCH 4/9] [Console] added support for multiline formats in ProgressBar --- .../Component/Console/Helper/ProgressBar.php | 13 +++++++--- .../Console/Tests/Helper/ProgressBarTest.php | 24 ++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index b96fbfa86bbbd..6b1b77e291857 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -48,6 +48,7 @@ class ProgressBar private $percent; private $lastMessagesLength; private $barCharOriginal; + private $formatLineCount; static private $formatters; @@ -224,6 +225,7 @@ public function getProgressCharacter() public function setFormat($format) { $this->format = $format; + $this->formatLineCount = substr_count($format, "\n"); } /** @@ -248,7 +250,7 @@ public function start() $this->barCharOriginal = ''; if (null === $this->format) { - $this->format = $this->determineBestFormat(); + $this->setFormat($this->determineBestFormat()); } if (!$this->max) { @@ -351,7 +353,7 @@ public function display() */ public function clear() { - $this->overwrite(''); + $this->overwrite(str_repeat("\n", $this->formatLineCount)); } /** @@ -364,12 +366,17 @@ private function overwrite($message) $length = Helper::strlen($message); // append whitespace to match the last line's length +// FIXME: on each line!!!!! +// FIXME: max of each line for lastMessagesLength or an array? if (null !== $this->lastMessagesLength && $this->lastMessagesLength > $length) { $message = str_pad($message, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT); } - // carriage return + // move back to the beginning of the progress bar before redrawing it $this->output->write("\x0D"); + if ($this->formatLineCount) { + $this->output->write(sprintf("\033[%dA", $this->formatLineCount)); + } $this->output->write($message); $this->lastMessagesLength = Helper::strlen($message); diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index 73a2ae0dd2de0..9b11adddfb271 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -319,6 +319,26 @@ public function testAddingPlaceholderFormatter() ); } + public function testMultilineFormat() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 3); + $bar->setFormat("%bar%\nfoobar"); + + $bar->start(); + $bar->advance(); + $bar->clear(); + $bar->finish(); + + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(">---------------------------\nfoobar"). + $this->generateOutput("=========>------------------\nfoobar"). + $this->generateOutput("\n"). + $this->generateOutput("============================\nfoobar"), + stream_get_contents($output->getStream()) + ); + } + protected function getOutputStream($decorated = true) { return new StreamOutput(fopen('php://memory', 'r+', false), StreamOutput::VERBOSITY_NORMAL, $decorated); @@ -334,6 +354,8 @@ protected function generateOutput($expected) $this->lastMessagesLength = strlen($expectedout); - return "\x0D".$expectedout; + $count = substr_count($expected, "\n"); + + return "\x0D".($count ? sprintf("\033[%dA", $count) : '').$expectedout; } } From a9d47ebbf29976a0a738a0c6aa7f7904ba0233cd Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 1 Mar 2014 09:14:29 +0100 Subject: [PATCH 5/9] [Console] added a way to add a custom message on a progress bar --- .../Component/Console/Helper/ProgressBar.php | 60 +++++++++++++------ .../Console/Tests/Helper/ProgressBarTest.php | 6 +- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 6b1b77e291857..93bc394a060ce 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -23,11 +23,11 @@ class ProgressBar { const FORMAT_QUIET = ' %percent%%'; - const FORMAT_NORMAL = ' %current%/%max% [%bar%] %percent%%'; - const FORMAT_VERBOSE = ' %current%/%max% [%bar%] %percent%% Elapsed: %elapsed%'; + const FORMAT_NORMAL = ' %current%/%max% [%bar%] %percent:3s%%'; + const FORMAT_VERBOSE = ' %current%/%max% [%bar%] %percent:3s%% Elapsed: %elapsed:6s%'; const FORMAT_QUIET_NOMAX = ' %current%'; const FORMAT_NORMAL_NOMAX = ' %current% [%bar%]'; - const FORMAT_VERBOSE_NOMAX = ' %current% [%bar%] Elapsed: %elapsed%'; + const FORMAT_VERBOSE_NOMAX = ' %current% [%bar%] Elapsed: %elapsed:6s%'; // options private $barWidth = 28; @@ -49,6 +49,7 @@ class ProgressBar private $lastMessagesLength; private $barCharOriginal; private $formatLineCount; + private $messages; static private $formatters; @@ -87,6 +88,16 @@ public static function setPlaceholderFormatter($name, $callable) self::$formatters[$name] = $callable; } + public function setMessage($message, $name = 'message') + { + $this->messages[$name] = $message; + } + + public function getMessage($name = 'message') + { + return $this->messages[$name]; + } + /** * Gets the progress bar start time. * @@ -337,10 +348,21 @@ public function display() throw new \LogicException('You must start the progress bar before calling display().'); } - $regex = implode('|', array_keys(self::$formatters)); $self = $this; - $this->overwrite(preg_replace_callback("{($regex)}", function ($matches) use ($self) { - return call_user_func(self::$formatters[$matches[1]], $self); + $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self) { + if (isset(self::$formatters[$matches[1]])) { + $text = call_user_func(self::$formatters[$matches[1]], $self); + } elseif (isset($this->messages[$matches[1]])) { + $text = $this->messages[$matches[1]]; + } else { + return $matches[0]; + } + + if (isset($matches[2])) { + $text = sprintf('%'.$matches[2], $text); + } + + return $text; }, $this->format)); } @@ -413,7 +435,7 @@ private function determineBestFormat() static private function initPlaceholderFormatters() { return array( - '%bar%' => function (ProgressBar $bar) { + 'bar' => function (ProgressBar $bar) { $completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getStep() % $bar->getBarWidth()); $emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlen($bar->getProgressCharacter()); $display = str_repeat($bar->getBarCharacter(), $completeBars); @@ -423,10 +445,10 @@ static private function initPlaceholderFormatters() return $display; }, - '%elapsed%' => function (ProgressBar $bar) { - return str_pad(Helper::formatTime(time() - $bar->getStartTime()), 6, ' ', STR_PAD_LEFT); + 'elapsed' => function (ProgressBar $bar) { + return Helper::formatTime(time() - $bar->getStartTime()); }, - '%remaining%' => function (ProgressBar $bar) { + 'remaining' => function (ProgressBar $bar) { if (!$bar->getMaxSteps()) { throw new \LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); } @@ -437,9 +459,9 @@ static private function initPlaceholderFormatters() $remaining = round((time() - $bar->getStartTime()) / $bar->getStep() * ($bar->getMaxSteps() - $bar->getStep())); } - return str_pad(Helper::formatTime($remaining), 6, ' ', STR_PAD_LEFT); + return Helper::formatTime($remaining); }, - '%estimated%' => function (ProgressBar $bar) { + 'estimated' => function (ProgressBar $bar) { if (!$bar->getMaxSteps()) { throw new \LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); } @@ -450,19 +472,19 @@ static private function initPlaceholderFormatters() $estimated = round((time() - $bar->getStartTime()) / $bar->getStep() * $bar->getMaxSteps()); } - return str_pad(Helper::formatTime($estimated), 6, ' ', STR_PAD_LEFT); + return Helper::formatTime($estimated); }, - '%memory%' => function (ProgressBar $bar) { - return str_pad(Helper::formatMemory(memory_get_usage(true)), 6, ' ', STR_PAD_LEFT);; + 'memory' => function (ProgressBar $bar) { + return Helper::formatMemory(memory_get_usage(true)); }, - '%current%' => function (ProgressBar $bar) { + 'current' => function (ProgressBar $bar) { return str_pad($bar->getStep(), $bar->getStepWidth(), ' ', STR_PAD_LEFT); }, - '%max%' => function (ProgressBar $bar) { + 'max' => function (ProgressBar $bar) { return $bar->getMaxSteps(); }, - '%percent%' => function (ProgressBar $bar) { - return str_pad(floor($bar->getProgressPercent() * 100), 3, ' ', STR_PAD_LEFT); + 'percent' => function (ProgressBar $bar) { + return floor($bar->getProgressPercent() * 100); }, ); } diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index 9b11adddfb271..527b81c11eec6 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -69,7 +69,7 @@ public function testCustomizations() $bar->setBarCharacter('_'); $bar->setEmptyBarCharacter(' '); $bar->setProgressCharacter('/'); - $bar->setFormat(' %current%/%max% [%bar%] %percent%%'); + $bar->setFormat(' %current%/%max% [%bar%] %percent:3s%%'); $bar->start(); $bar->advance(); @@ -102,7 +102,7 @@ public function testPercent() public function testOverwriteWithShorterLine() { $bar = new ProgressBar($output = $this->getOutputStream(), 50); - $bar->setFormat(' %current%/%max% [%bar%] %percent%%'); + $bar->setFormat(' %current%/%max% [%bar%] %percent:3s%%'); $bar->start(); $bar->display(); $bar->advance(); @@ -300,7 +300,7 @@ public function testParallelBars() public function testAddingPlaceholderFormatter() { - ProgressBar::setPlaceholderFormatter('%remaining_steps%', function (ProgressBar $bar) { + ProgressBar::setPlaceholderFormatter('remaining_steps', function (ProgressBar $bar) { return $bar->getMaxSteps() - $bar->getStep(); }); $bar = new ProgressBar($output = $this->getOutputStream(), 3); From 244d3b81be40ebdd83f70f9b9a6587605f5b9355 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 1 Mar 2014 17:52:22 +0100 Subject: [PATCH 6/9] [Console] added a way to globally add a progress bar format or modify a built-in one --- .../Component/Console/Helper/ProgressBar.php | 67 +++++++++++-------- .../Console/Tests/Helper/ProgressBarTest.php | 2 +- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 93bc394a060ce..5bf5cd124bb4b 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -22,13 +22,6 @@ */ class ProgressBar { - const FORMAT_QUIET = ' %percent%%'; - const FORMAT_NORMAL = ' %current%/%max% [%bar%] %percent:3s%%'; - const FORMAT_VERBOSE = ' %current%/%max% [%bar%] %percent:3s%% Elapsed: %elapsed:6s%'; - const FORMAT_QUIET_NOMAX = ' %current%'; - const FORMAT_NORMAL_NOMAX = ' %current% [%bar%]'; - const FORMAT_VERBOSE_NOMAX = ' %current% [%bar%] Elapsed: %elapsed:6s%'; - // options private $barWidth = 28; private $barChar = '='; @@ -52,6 +45,7 @@ class ProgressBar private $messages; static private $formatters; + static private $formats; /** * Constructor. @@ -69,6 +63,10 @@ public function __construct(OutputInterface $output, $max = 0) if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } + + if (!self::$formats) { + self::$formats = self::initFormats(); + } } /** @@ -79,7 +77,7 @@ public function __construct(OutputInterface $output, $max = 0) * @param string $name The placeholder name (including the delimiter char like %) * @param callable $callable A PHP callable */ - public static function setPlaceholderFormatter($name, $callable) + public static function setPlaceholderFormatterDefinition($name, $callable) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); @@ -88,6 +86,23 @@ public static function setPlaceholderFormatter($name, $callable) self::$formatters[$name] = $callable; } + /** + * Sets a format for a given name. + * + * This method also allow you to override an existing format. + * + * @param string $name The format name + * @param string $format A format string + */ + public static function setFormatDefinition($name, $format) + { + if (!self::$formats) { + self::$formats = self::initFormats(); + } + + self::$formats[$name] = $format; + } + public function setMessage($message, $name = 'message') { $this->messages[$name] = $message; @@ -235,8 +250,8 @@ public function getProgressCharacter() */ public function setFormat($format) { - $this->format = $format; - $this->formatLineCount = substr_count($format, "\n"); + $this->format = isset(self::$formats[$format]) ? self::$formats[$format] : $format; + $this->formatLineCount = substr_count($this->format, "\n"); } /** @@ -408,28 +423,14 @@ private function determineBestFormat() { switch ($this->output->getVerbosity()) { case OutputInterface::VERBOSITY_QUIET: - $format = self::FORMAT_QUIET_NOMAX; - if ($this->max > 0) { - $format = self::FORMAT_QUIET; - } - break; + return $this->max > 0 ? 'quiet' : 'quiet_nomax'; case OutputInterface::VERBOSITY_VERBOSE: case OutputInterface::VERBOSITY_VERY_VERBOSE: case OutputInterface::VERBOSITY_DEBUG: - $format = self::FORMAT_VERBOSE_NOMAX; - if ($this->max > 0) { - $format = self::FORMAT_VERBOSE; - } - break; + return $this->max > 0 ? 'verbose' : 'verbose_nomax'; default: - $format = self::FORMAT_NORMAL_NOMAX; - if ($this->max > 0) { - $format = self::FORMAT_NORMAL; - } - break; + return $this->max > 0 ? 'normal' : 'normal_nomax'; } - - return $format; } static private function initPlaceholderFormatters() @@ -488,4 +489,16 @@ static private function initPlaceholderFormatters() }, ); } + + static private function initFormats() + { + return array( + 'quiet' => ' %percent%%', + 'normal' => ' %current%/%max% [%bar%] %percent:3s%%', + 'verbose' => ' %current%/%max% [%bar%] %percent:3s%% Elapsed: %elapsed:6s%', + 'quiet_nomax' => ' %current%', + 'normal_nomax' => ' %current% [%bar%]', + 'verbose_nomax' => ' %current% [%bar%] Elapsed: %elapsed:6s%', + ); + } } diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index 527b81c11eec6..8e18c512c061b 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -300,7 +300,7 @@ public function testParallelBars() public function testAddingPlaceholderFormatter() { - ProgressBar::setPlaceholderFormatter('remaining_steps', function (ProgressBar $bar) { + ProgressBar::setPlaceholderFormatterDefinition('remaining_steps', function (ProgressBar $bar) { return $bar->getMaxSteps() - $bar->getStep(); }); $bar = new ProgressBar($output = $this->getOutputStream(), 3); From 38f7a6f81795f2ef5d40501366aa8a559acc2c0c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 1 Mar 2014 18:08:18 +0100 Subject: [PATCH 7/9] [Console] fixed PHP comptability --- .../Component/Console/Helper/ProgressBar.php | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 5bf5cd124bb4b..b1c7d67d32dd4 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -44,8 +44,8 @@ class ProgressBar private $formatLineCount; private $messages; - static private $formatters; - static private $formats; + private static $formatters; + private static $formats; /** * Constructor. @@ -86,6 +86,18 @@ public static function setPlaceholderFormatterDefinition($name, $callable) self::$formatters[$name] = $callable; } + /** + * 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($name) + { + return isset(self::$formatters[$name]) ? self::$formatters[$name] : null; + } + /** * Sets a format for a given name. * @@ -103,6 +115,18 @@ public static function setFormatDefinition($name, $format) self::$formats[$name] = $format; } + /** + * Gets the format for a given name. + * + * @param string $name The format name + * + * @return string|null A format string + */ + public static function getFormatDefinition($name) + { + return isset(self::$formats[$name]) ? self::$formats[$name] : null; + } + public function setMessage($message, $name = 'message') { $this->messages[$name] = $message; @@ -116,7 +140,7 @@ public function getMessage($name = 'message') /** * Gets the progress bar start time. * - * @return int The progress bar start time + * @return integer The progress bar start time */ public function getStartTime() { @@ -126,7 +150,7 @@ public function getStartTime() /** * Gets the progress bar maximal steps. * - * @return int The progress bar max steps + * @return integer The progress bar max steps */ public function getMaxSteps() { @@ -136,7 +160,7 @@ public function getMaxSteps() /** * Gets the progress bar step. * - * @return int The progress bar step + * @return integer The progress bar step */ public function getStep() { @@ -146,7 +170,7 @@ public function getStep() /** * Gets the progress bar step width. * - * @return int The progress bar step width + * @return integer The progress bar step width */ public function getStepWidth() { @@ -156,7 +180,7 @@ public function getStepWidth() /** * Gets the current progress bar percent. * - * @return int The current progress bar percent + * @return integer The current progress bar percent */ public function getProgressPercent() { @@ -166,7 +190,7 @@ public function getProgressPercent() /** * Sets the progress bar width. * - * @param int $size The progress bar size + * @param integer $size The progress bar size */ public function setBarWidth($size) { @@ -176,7 +200,7 @@ public function setBarWidth($size) /** * Gets the progress bar width. * - * @return int The progress bar size + * @return integer The progress bar size */ public function getBarWidth() { @@ -257,7 +281,7 @@ public function setFormat($format) /** * Sets the redraw frequency. * - * @param int $freq The frequency in steps + * @param integer $freq The frequency in steps */ public function setRedrawFrequency($freq) { @@ -365,8 +389,8 @@ public function display() $self = $this; $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self) { - if (isset(self::$formatters[$matches[1]])) { - $text = call_user_func(self::$formatters[$matches[1]], $self); + if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) { + $text = call_user_func($formatter, $self); } elseif (isset($this->messages[$matches[1]])) { $text = $this->messages[$matches[1]]; } else { @@ -433,7 +457,7 @@ private function determineBestFormat() } } - static private function initPlaceholderFormatters() + private static function initPlaceholderFormatters() { return array( 'bar' => function (ProgressBar $bar) { @@ -490,7 +514,7 @@ static private function initPlaceholderFormatters() ); } - static private function initFormats() + private static function initFormats() { return array( 'quiet' => ' %percent%%', From 8c0022b769babc99e5d7761cacf5416a7ffb6e17 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 1 Mar 2014 22:26:36 +0100 Subject: [PATCH 8/9] [Console] fixed progress bar when using ANSI colors and Emojis --- .../Component/Console/Helper/Helper.php | 15 +++++ .../Component/Console/Helper/ProgressBar.php | 39 +++++++----- .../Component/Console/Helper/TableHelper.php | 16 +---- .../Console/Tests/Helper/ProgressBarTest.php | 63 +++++++++++++++---- 4 files changed, 92 insertions(+), 41 deletions(-) diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index cdeca3523a68d..39d5d884620a9 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Console\Helper; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + /** * Helper is the base class for all helper classes. * @@ -103,4 +105,17 @@ public static function formatMemory($memory) return sprintf('%d B', $memory); } + + public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, $string) + { + $isDecorated = $formatter->isDecorated(); + $formatter->setDecorated(false); + // remove <...> formatting + $string = $formatter->format($string); + // remove already formatted characters + $string = preg_replace("/\033\[[^m]*m/", '', $string); + $formatter->setDecorated($isDecorated); + + return self::strlen($string); + } } diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index b1c7d67d32dd4..03c1df47f8447 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -387,12 +387,15 @@ public function display() throw new \LogicException('You must start the progress bar before calling display().'); } + // these 3 variables can be removed in favor of using $this in the closure when support for PHP 5.3 will be dropped. $self = $this; - $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self) { + $output = $this->output; + $messages = $this->messages; + $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self, $output, $messages) { if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) { - $text = call_user_func($formatter, $self); - } elseif (isset($this->messages[$matches[1]])) { - $text = $this->messages[$matches[1]]; + $text = call_user_func($formatter, $self, $output); + } elseif (isset($messages[$matches[1]])) { + $text = $messages[$matches[1]]; } else { return $matches[0]; } @@ -424,13 +427,15 @@ public function clear() */ private function overwrite($message) { - $length = Helper::strlen($message); + $lines = explode("\n", $message); - // append whitespace to match the last line's length -// FIXME: on each line!!!!! -// FIXME: max of each line for lastMessagesLength or an array? - if (null !== $this->lastMessagesLength && $this->lastMessagesLength > $length) { - $message = str_pad($message, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT); + // append whitespace to match the line's length + if (null !== $this->lastMessagesLength) { + foreach ($lines as $i => $line) { + if ($this->lastMessagesLength > Helper::strlenWithoutDecoration($this->output->getFormatter(), $line)) { + $lines[$i] = str_pad($line, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT); + } + } } // move back to the beginning of the progress bar before redrawing it @@ -438,9 +443,15 @@ private function overwrite($message) if ($this->formatLineCount) { $this->output->write(sprintf("\033[%dA", $this->formatLineCount)); } - $this->output->write($message); + $this->output->write(implode("\n", $lines)); - $this->lastMessagesLength = Helper::strlen($message); + $this->lastMessagesLength = 0; + foreach ($lines as $line) { + $len = Helper::strlenWithoutDecoration($this->output->getFormatter(), $line); + if ($len > $this->lastMessagesLength) { + $this->lastMessagesLength = $len; + } + } } private function determineBestFormat() @@ -460,11 +471,11 @@ private function determineBestFormat() private static function initPlaceholderFormatters() { return array( - 'bar' => function (ProgressBar $bar) { + 'bar' => function (ProgressBar $bar, OutputInterface $output) { $completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getStep() % $bar->getBarWidth()); - $emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlen($bar->getProgressCharacter()); $display = str_repeat($bar->getBarCharacter(), $completeBars); if ($completeBars < $bar->getBarWidth()) { + $emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter()); $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars); } diff --git a/src/Symfony/Component/Console/Helper/TableHelper.php b/src/Symfony/Component/Console/Helper/TableHelper.php index d6ad0e9e01804..36237c8323b5e 100644 --- a/src/Symfony/Component/Console/Helper/TableHelper.php +++ b/src/Symfony/Component/Console/Helper/TableHelper.php @@ -427,7 +427,7 @@ private function renderCell(array $row, $column, $cellFormat) $width += strlen($cell) - mb_strlen($cell, $encoding); } - $width += $this->strlen($cell) - $this->computeLengthWithoutDecoration($cell); + $width += $this->strlen($cell) - self::strlenWithoutDecoration($this->output->getFormatter(), $cell); $content = sprintf($this->cellRowContentFormat, $cell); @@ -486,7 +486,7 @@ private function getColumnWidth($column) */ private function getCellWidth(array $row, $column) { - return isset($row[$column]) ? $this->computeLengthWithoutDecoration($row[$column]) : 0; + return isset($row[$column]) ? self::strlenWithoutDecoration($this->output->getFormatter(), $row[$column]) : 0; } /** @@ -498,18 +498,6 @@ private function cleanup() $this->numberOfColumns = null; } - private function computeLengthWithoutDecoration($string) - { - $formatter = $this->output->getFormatter(); - $isDecorated = $formatter->isDecorated(); - $formatter->setDecorated(false); - - $string = $formatter->format($string); - $formatter->setDecorated($isDecorated); - - return $this->strlen($string); - } - /** * {@inheritDoc} */ diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index 8e18c512c061b..152f6939113af 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Console\Tests\Helper; use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Output\StreamOutput; class ProgressBarTest extends \PHPUnit_Framework_TestCase @@ -206,7 +207,7 @@ public function testClear() $this->assertEquals( $this->generateOutput(' 0/50 [>---------------------------] 0%'). $this->generateOutput(' 25/50 [==============>-------------] 50%'). - $this->generateOutput(''), + $this->generateOutput(' '), stream_get_contents($output->getStream()) ); } @@ -332,9 +333,53 @@ public function testMultilineFormat() rewind($output->getStream()); $this->assertEquals( $this->generateOutput(">---------------------------\nfoobar"). - $this->generateOutput("=========>------------------\nfoobar"). - $this->generateOutput("\n"). - $this->generateOutput("============================\nfoobar"), + $this->generateOutput("=========>------------------\nfoobar "). + $this->generateOutput(" \n "). + $this->generateOutput("============================\nfoobar "), + stream_get_contents($output->getStream()) + ); + } + + public function testAnsiColorsAndEmojis() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 15); + ProgressBar::setPlaceholderFormatterDefinition('memory', function (ProgressBar $bar) { + static $i = 0; + $mem = 100000 * $i; + $colors = $i++ ? '41;37' : '44;37'; + + return "\033[".$colors."m ".Helper::formatMemory($mem)." \033[0m"; + }); + $bar->setFormat(" \033[44;37m %title:-37s% \033[0m\n %current%/%max% %bar% %percent:3s%%\n 🏁 %remaining:-10s% %memory:37s%"); + $bar->setBarCharacter($done = "\033[32m●\033[0m"); + $bar->setEmptyBarCharacter($empty = "\033[31m●\033[0m"); + $bar->setProgressCharacter($progress = "\033[32m➤ \033[0m"); + + $bar->setMessage('Starting the demo... fingers crossed', 'title'); + $bar->start(); + $bar->setMessage('Looks good to me...', 'title'); + $bar->advance(4); + $bar->setMessage('Thanks, bye', 'title'); + $bar->finish(); + + rewind($output->getStream()); + + $this->assertEquals( + $this->generateOutput( + " \033[44;37m Starting the demo... fingers crossed \033[0m\n". + " 0/15 ".$progress.str_repeat($empty, 26)." 0%\n". + " 🏁 1 sec \033[44;37m 0 B \033[0m" + ). + $this->generateOutput( + " \033[44;37m Looks good to me... \033[0m\n". + " 4/15 ".str_repeat($done, 7).$progress.str_repeat($empty, 19)." 26%\n". + " 🏁 1 sec \033[41;37m 97 kB \033[0m" + ). + $this->generateOutput( + " \033[44;37m Thanks, bye \033[0m\n". + " 15/15 ".str_repeat($done, 28)." 100%\n". + " 🏁 1 sec \033[41;37m 195 kB \033[0m" + ), stream_get_contents($output->getStream()) ); } @@ -346,16 +391,8 @@ protected function getOutputStream($decorated = true) protected function generateOutput($expected) { - $expectedout = $expected; - - if (null !== $this->lastMessagesLength) { - $expectedout = str_pad($expected, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT); - } - - $this->lastMessagesLength = strlen($expectedout); - $count = substr_count($expected, "\n"); - return "\x0D".($count ? sprintf("\033[%dA", $count) : '').$expectedout; + return "\x0D".($count ? sprintf("\033[%dA", $count) : '').$expected; } } From 0d1a58c4698ede3543ff755ad63aa687b101d03f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 3 Mar 2014 10:33:57 +0100 Subject: [PATCH 9/9] [Console] made formats even more flexible --- .../Component/Console/Helper/ProgressBar.php | 40 ++++++++++++------- .../Console/Tests/Helper/ProgressBarTest.php | 22 +++++++++- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 03c1df47f8447..8cd7ba34e9704 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -67,6 +67,8 @@ public function __construct(OutputInterface $output, $max = 0) if (!self::$formats) { self::$formats = self::initFormats(); } + + $this->setFormat($this->determineBestFormat()); } /** @@ -274,7 +276,15 @@ public function getProgressCharacter() */ public function setFormat($format) { - $this->format = isset(self::$formats[$format]) ? self::$formats[$format] : $format; + // try to use the _nomax variant if available + if (!$this->max && isset(self::$formats[$format.'_nomax'])) { + $this->format = self::$formats[$format.'_nomax']; + } elseif (isset(self::$formats[$format])) { + $this->format = self::$formats[$format]; + } else { + $this->format = $format; + } + $this->formatLineCount = substr_count($this->format, "\n"); } @@ -299,10 +309,6 @@ public function start() $this->lastMessagesLength = 0; $this->barCharOriginal = ''; - if (null === $this->format) { - $this->setFormat($this->determineBestFormat()); - } - if (!$this->max) { $this->barCharOriginal = $this->barChar; $this->barChar = $this->emptyBarChar; @@ -457,12 +463,13 @@ private function overwrite($message) private function determineBestFormat() { switch ($this->output->getVerbosity()) { - case OutputInterface::VERBOSITY_QUIET: - return $this->max > 0 ? 'quiet' : 'quiet_nomax'; + // OutputInterface::VERBOSITY_QUIET: display is disabled anyway case OutputInterface::VERBOSITY_VERBOSE: + return $this->max > 0 ? 'verbose' : 'verbose_nomax'; case OutputInterface::VERBOSITY_VERY_VERBOSE: + return $this->max > 0 ? 'very_verbose' : 'very_verbose_nomax'; case OutputInterface::VERBOSITY_DEBUG: - return $this->max > 0 ? 'verbose' : 'verbose_nomax'; + return $this->max > 0 ? 'debug' : 'debug_nomax'; default: return $this->max > 0 ? 'normal' : 'normal_nomax'; } @@ -528,12 +535,17 @@ private static function initPlaceholderFormatters() private static function initFormats() { return array( - 'quiet' => ' %percent%%', - 'normal' => ' %current%/%max% [%bar%] %percent:3s%%', - 'verbose' => ' %current%/%max% [%bar%] %percent:3s%% Elapsed: %elapsed:6s%', - 'quiet_nomax' => ' %current%', - 'normal_nomax' => ' %current% [%bar%]', - 'verbose_nomax' => ' %current% [%bar%] Elapsed: %elapsed:6s%', + 'normal' => ' %current%/%max% [%bar%] %percent:3s%%', + 'normal_nomax' => ' %current% [%bar%]', + + 'verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', + 'verbose_nomax' => ' %current% [%bar%] %percent:3s%% %elapsed:6s%', + + 'very_verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', + 'very_verbose_nomax' => ' %current% [%bar%] %percent:3s%% %elapsed:6s%', + + 'debug' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', + 'debug_nomax' => ' %current% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', ); } } diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index 152f6939113af..7bcfed026b9a9 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -363,7 +363,6 @@ public function testAnsiColorsAndEmojis() $bar->finish(); rewind($output->getStream()); - $this->assertEquals( $this->generateOutput( " \033[44;37m Starting the demo... fingers crossed \033[0m\n". @@ -384,6 +383,27 @@ public function testAnsiColorsAndEmojis() ); } + public function testSetFormat() + { + $bar = new ProgressBar($output = $this->getOutputStream()); + $bar->setFormat('normal'); + $bar->start(); + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0 [>---------------------------]'), + stream_get_contents($output->getStream()) + ); + + $bar = new ProgressBar($output = $this->getOutputStream(), 10); + $bar->setFormat('normal'); + $bar->start(); + rewind($output->getStream()); + $this->assertEquals( + $this->generateOutput(' 0/10 [>---------------------------] 0%'), + stream_get_contents($output->getStream()) + ); + } + protected function getOutputStream($decorated = true) { return new StreamOutput(fopen('php://memory', 'r+', false), StreamOutput::VERBOSITY_NORMAL, $decorated);