Skip to content

Commit 69af9ea

Browse files
committed
feature #16409 [Console] Add progress indicator helper (kbond)
This PR was merged into the 2.8 branch. Discussion ---------- [Console] Add progress indicator helper | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #12119 | License | MIT | Doc PR | Commits ------- be0a364 [Console] Add progress indicator helper
2 parents ed4e3e8 + be0a364 commit 69af9ea

File tree

2 files changed

+501
-0
lines changed

2 files changed

+501
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Helper;
13+
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
16+
/**
17+
* @author Kevin Bond <kevinbond@gmail.com>
18+
*/
19+
class ProgressIndicator
20+
{
21+
private $output;
22+
private $startTime;
23+
private $format;
24+
private $message;
25+
private $indicatorValues;
26+
private $indicatorCurrent;
27+
private $indicatorChangeInterval;
28+
private $indicatorUpdateTime;
29+
private $lastMessagesLength;
30+
private $started = false;
31+
32+
private static $formatters;
33+
private static $formats;
34+
35+
/**
36+
* @param OutputInterface $output
37+
* @param string|null $format Indicator format
38+
* @param int $indicatorChangeInterval Change interval in milliseconds
39+
* @param array|null $indicatorValues Animated indicator characters
40+
*/
41+
public function __construct(OutputInterface $output, $format = null, $indicatorChangeInterval = 100, $indicatorValues = null)
42+
{
43+
$this->output = $output;
44+
45+
if (null === $format) {
46+
$format = $this->determineBestFormat();
47+
}
48+
49+
if (null === $indicatorValues) {
50+
$indicatorValues = array('-', '\\', '|', '/');
51+
}
52+
53+
$indicatorValues = array_values($indicatorValues);
54+
55+
if (2 > count($indicatorValues)) {
56+
throw new \InvalidArgumentException('Must have at least 2 indicator value characters.');
57+
}
58+
59+
$this->format = self::getFormatDefinition($format);
60+
$this->indicatorChangeInterval = $indicatorChangeInterval;
61+
$this->indicatorValues = $indicatorValues;
62+
$this->startTime = time();
63+
}
64+
65+
/**
66+
* Sets the current indicator message.
67+
*
68+
* @param string|null $message
69+
*/
70+
public function setMessage($message)
71+
{
72+
$this->message = $message;
73+
74+
$this->display();
75+
}
76+
77+
/**
78+
* Gets the current indicator message.
79+
*
80+
* @return string|null
81+
*
82+
* @internal for PHP 5.3 compatibility
83+
*/
84+
public function getMessage()
85+
{
86+
return $this->message;
87+
}
88+
89+
/**
90+
* Gets the progress bar start time.
91+
*
92+
* @return int The progress bar start time
93+
*
94+
* @internal for PHP 5.3 compatibility
95+
*/
96+
public function getStartTime()
97+
{
98+
return $this->startTime;
99+
}
100+
101+
/**
102+
* Gets the current animated indicator character.
103+
*
104+
* @return string
105+
*
106+
* @internal for PHP 5.3 compatibility
107+
*/
108+
public function getCurrentValue()
109+
{
110+
return $this->indicatorValues[$this->indicatorCurrent % count($this->indicatorValues)];
111+
}
112+
113+
/**
114+
* Starts the indicator output.
115+
*
116+
* @param $message
117+
*/
118+
public function start($message)
119+
{
120+
if ($this->started) {
121+
throw new \LogicException('Progress indicator already started.');
122+
}
123+
124+
$this->message = $message;
125+
$this->started = true;
126+
$this->lastMessagesLength = 0;
127+
$this->startTime = time();
128+
$this->indicatorUpdateTime = $this->getCurrentTimeInMilliseconds() + $this->indicatorChangeInterval;
129+
$this->indicatorCurrent = 0;
130+
131+
$this->display();
132+
}
133+
134+
/**
135+
* Advances the indicator.
136+
*/
137+
public function advance()
138+
{
139+
if (!$this->started) {
140+
throw new \LogicException('Progress indicator has not yet been started.');
141+
}
142+
143+
if (!$this->output->isDecorated()) {
144+
return;
145+
}
146+
147+
$currentTime = $this->getCurrentTimeInMilliseconds();
148+
149+
if ($currentTime < $this->indicatorUpdateTime) {
150+
return;
151+
}
152+
153+
$this->indicatorUpdateTime = $currentTime + $this->indicatorChangeInterval;
154+
++$this->indicatorCurrent;
155+
156+
$this->display();
157+
}
158+
159+
/**
160+
* Finish the indicator with message.
161+
*
162+
* @param $message
163+
*/
164+
public function finish($message)
165+
{
166+
if (!$this->started) {
167+
throw new \LogicException('Progress indicator has not yet been started.');
168+
}
169+
170+
$this->message = $message;
171+
$this->display();
172+
$this->output->writeln('');
173+
$this->started = false;
174+
}
175+
176+
/**
177+
* Gets the format for a given name.
178+
*
179+
* @param string $name The format name
180+
*
181+
* @return string|null A format string
182+
*/
183+
public static function getFormatDefinition($name)
184+
{
185+
if (!self::$formats) {
186+
self::$formats = self::initFormats();
187+
}
188+
189+
return isset(self::$formats[$name]) ? self::$formats[$name] : null;
190+
}
191+
192+
/**
193+
* Sets a placeholder formatter for a given name.
194+
*
195+
* This method also allow you to override an existing placeholder.
196+
*
197+
* @param string $name The placeholder name (including the delimiter char like %)
198+
* @param callable $callable A PHP callable
199+
*/
200+
public static function setPlaceholderFormatterDefinition($name, $callable)
201+
{
202+
if (!self::$formatters) {
203+
self::$formatters = self::initPlaceholderFormatters();
204+
}
205+
206+
self::$formatters[$name] = $callable;
207+
}
208+
209+
/**
210+
* Gets the placeholder formatter for a given name.
211+
*
212+
* @param string $name The placeholder name (including the delimiter char like %)
213+
*
214+
* @return callable|null A PHP callable
215+
*/
216+
public static function getPlaceholderFormatterDefinition($name)
217+
{
218+
if (!self::$formatters) {
219+
self::$formatters = self::initPlaceholderFormatters();
220+
}
221+
222+
return isset(self::$formatters[$name]) ? self::$formatters[$name] : null;
223+
}
224+
225+
private function display()
226+
{
227+
if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) {
228+
return;
229+
}
230+
231+
$self = $this;
232+
233+
$this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self) {
234+
if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) {
235+
return call_user_func($formatter, $self);
236+
}
237+
238+
return $matches[0];
239+
}, $this->format));
240+
}
241+
242+
private function determineBestFormat()
243+
{
244+
switch ($this->output->getVerbosity()) {
245+
// OutputInterface::VERBOSITY_QUIET: display is disabled anyway
246+
case OutputInterface::VERBOSITY_VERBOSE:
247+
return $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi';
248+
case OutputInterface::VERBOSITY_VERY_VERBOSE:
249+
case OutputInterface::VERBOSITY_DEBUG:
250+
return $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi';
251+
default:
252+
return $this->output->isDecorated() ? 'normal' : 'normal_no_ansi';
253+
}
254+
}
255+
256+
/**
257+
* Overwrites a previous message to the output.
258+
*
259+
* @param string $message The message
260+
*/
261+
private function overwrite($message)
262+
{
263+
// append whitespace to match the line's length
264+
if (null !== $this->lastMessagesLength) {
265+
if ($this->lastMessagesLength > Helper::strlenWithoutDecoration($this->output->getFormatter(), $message)) {
266+
$message = str_pad($message, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT);
267+
}
268+
}
269+
270+
if ($this->output->isDecorated()) {
271+
$this->output->write("\x0D");
272+
$this->output->write($message);
273+
} else {
274+
$this->output->writeln($message);
275+
}
276+
277+
$this->lastMessagesLength = 0;
278+
279+
$len = Helper::strlenWithoutDecoration($this->output->getFormatter(), $message);
280+
281+
if ($len > $this->lastMessagesLength) {
282+
$this->lastMessagesLength = $len;
283+
}
284+
}
285+
286+
private function getCurrentTimeInMilliseconds()
287+
{
288+
return round(microtime(true) * 1000);
289+
}
290+
291+
private static function initPlaceholderFormatters()
292+
{
293+
return array(
294+
'indicator' => function (ProgressIndicator $indicator) {
295+
return $indicator->getCurrentValue();
296+
},
297+
'message' => function (ProgressIndicator $indicator) {
298+
return $indicator->getMessage();
299+
},
300+
'elapsed' => function (ProgressIndicator $indicator) {
301+
return Helper::formatTime(time() - $indicator->getStartTime());
302+
},
303+
'memory' => function () {
304+
return Helper::formatMemory(memory_get_usage(true));
305+
},
306+
);
307+
}
308+
309+
private static function initFormats()
310+
{
311+
return array(
312+
'normal' => ' %indicator% %message%',
313+
'normal_no_ansi' => ' %message%',
314+
315+
'verbose' => ' %indicator% %message% (%elapsed:6s%)',
316+
'verbose_no_ansi' => ' %message% (%elapsed:6s%)',
317+
318+
'very_verbose' => ' %indicator% %message% (%elapsed:6s%, %memory:6s%)',
319+
'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)',
320+
);
321+
}
322+
}

0 commit comments

Comments
 (0)