Completed
Push — develop ( f11ef2...d41b65 )
by David
06:01 queued 11s
created

ProgressBar::setPlaceholderFormatterDefinition()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
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\Cursor;
15
use Symfony\Component\Console\Exception\LogicException;
16
use Symfony\Component\Console\Output\ConsoleOutputInterface;
17
use Symfony\Component\Console\Output\ConsoleSectionOutput;
18
use Symfony\Component\Console\Output\OutputInterface;
19
use Symfony\Component\Console\Terminal;
20
21
/**
22
 * The ProgressBar provides helpers to display progress output.
23
 *
24
 * @author Fabien Potencier <[email protected]>
25
 * @author Chris Jones <[email protected]>
26
 */
27
final class ProgressBar
28
{
29
    private $barWidth = 28;
30
    private $barChar;
31
    private $emptyBarChar = '-';
32
    private $progressChar = '>';
33
    private $format;
34
    private $internalFormat;
35
    private $redrawFreq = 1;
36
    private $writeCount;
37
    private $lastWriteTime;
38
    private $minSecondsBetweenRedraws = 0;
39
    private $maxSecondsBetweenRedraws = 1;
40
    private $output;
41
    private $step = 0;
42
    private $max;
43
    private $startTime;
44
    private $stepWidth;
45
    private $percent = 0.0;
46
    private $formatLineCount;
47
    private $messages = [];
48
    private $overwrite = true;
49
    private $terminal;
50
    private $previousMessage;
51
    private $cursor;
52
53
    private static $formatters;
54
    private static $formats;
55
56
    /**
57
     * @param int $max Maximum steps (0 if unknown)
58
     */
59
    public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 0.1)
60
    {
61
        if ($output instanceof ConsoleOutputInterface) {
62
            $output = $output->getErrorOutput();
63
        }
64
65
        $this->output = $output;
66
        $this->setMaxSteps($max);
67
        $this->terminal = new Terminal();
68
69
        if (0 < $minSecondsBetweenRedraws) {
70
            $this->redrawFreq = null;
71
            $this->minSecondsBetweenRedraws = $minSecondsBetweenRedraws;
0 ignored issues
show
Documentation Bug introduced by
The property $minSecondsBetweenRedraws was declared of type integer, but $minSecondsBetweenRedraws is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
72
        }
73
74
        if (!$this->output->isDecorated()) {
75
            // disable overwrite when output does not support ANSI codes.
76
            $this->overwrite = false;
77
78
            // set a reasonable redraw frequency so output isn't flooded
79
            $this->redrawFreq = null;
80
        }
81
82
        $this->startTime = time();
83
        $this->cursor = new Cursor($output);
84
    }
85
86
    /**
87
     * Sets a placeholder formatter for a given name.
88
     *
89
     * This method also allow you to override an existing placeholder.
90
     *
91
     * @param string   $name     The placeholder name (including the delimiter char like %)
92
     * @param callable $callable A PHP callable
93
     */
94
    public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void
95
    {
96
        if (!self::$formatters) {
97
            self::$formatters = self::initPlaceholderFormatters();
98
        }
99
100
        self::$formatters[$name] = $callable;
101
    }
102
103
    /**
104
     * Gets the placeholder formatter for a given name.
105
     *
106
     * @param string $name The placeholder name (including the delimiter char like %)
107
     *
108
     * @return callable|null A PHP callable
109
     */
110
    public static function getPlaceholderFormatterDefinition(string $name): ?callable
111
    {
112
        if (!self::$formatters) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::$formatters of type array<string,callable> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
113
            self::$formatters = self::initPlaceholderFormatters();
114
        }
115
116
        return isset(self::$formatters[$name]) ? self::$formatters[$name] : null;
117
    }
118
119
    /**
120
     * Sets a format for a given name.
121
     *
122
     * This method also allow you to override an existing format.
123
     *
124
     * @param string $name   The format name
125
     * @param string $format A format string
126
     */
127
    public static function setFormatDefinition(string $name, string $format): void
128
    {
129
        if (!self::$formats) {
130
            self::$formats = self::initFormats();
131
        }
132
133
        self::$formats[$name] = $format;
134
    }
135
136
    /**
137
     * Gets the format for a given name.
138
     *
139
     * @param string $name The format name
140
     *
141
     * @return string|null A format string
142
     */
143
    public static function getFormatDefinition(string $name): ?string
144
    {
145
        if (!self::$formats) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::$formats of type array<string,string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
146
            self::$formats = self::initFormats();
147
        }
148
149
        return isset(self::$formats[$name]) ? self::$formats[$name] : null;
150
    }
151
152
    /**
153
     * Associates a text with a named placeholder.
154
     *
155
     * The text is displayed when the progress bar is rendered but only
156
     * when the corresponding placeholder is part of the custom format line
157
     * (by wrapping the name with %).
158
     *
159
     * @param string $message The text to associate with the placeholder
160
     * @param string $name    The name of the placeholder
161
     */
162
    public function setMessage(string $message, string $name = 'message')
163
    {
164
        $this->messages[$name] = $message;
165
    }
166
167
    public function getMessage(string $name = 'message')
168
    {
169
        return $this->messages[$name];
170
    }
171
172
    public function getStartTime(): int
173
    {
174
        return $this->startTime;
175
    }
176
177
    public function getMaxSteps(): int
178
    {
179
        return $this->max;
180
    }
181
182
    public function getProgress(): int
183
    {
184
        return $this->step;
185
    }
186
187
    private function getStepWidth(): int
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
188
    {
189
        return $this->stepWidth;
190
    }
191
192
    public function getProgressPercent(): float
193
    {
194
        return $this->percent;
195
    }
196
197
    public function getBarOffset(): float
198
    {
199
        return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? min(5, $this->barWidth / 15) * $this->writeCount : $this->step) % $this->barWidth);
200
    }
201
202
    public function getEstimated(): float
203
    {
204
        if (!$this->step) {
205
            return 0;
206
        }
207
208
        return round((time() - $this->startTime) / $this->step * $this->max);
209
    }
210
211
    public function getRemaining(): float
212
    {
213
        if (!$this->step) {
214
            return 0;
215
        }
216
217
        return round((time() - $this->startTime) / $this->step * ($this->max - $this->step));
218
    }
219
220
    public function setBarWidth(int $size)
221
    {
222
        $this->barWidth = max(1, $size);
223
    }
224
225
    public function getBarWidth(): int
226
    {
227
        return $this->barWidth;
228
    }
229
230
    public function setBarCharacter(string $char)
231
    {
232
        $this->barChar = $char;
233
    }
234
235
    public function getBarCharacter(): string
236
    {
237
        if (null === $this->barChar) {
238
            return $this->max ? '=' : $this->emptyBarChar;
239
        }
240
241
        return $this->barChar;
242
    }
243
244
    public function setEmptyBarCharacter(string $char)
245
    {
246
        $this->emptyBarChar = $char;
247
    }
248
249
    public function getEmptyBarCharacter(): string
250
    {
251
        return $this->emptyBarChar;
252
    }
253
254
    public function setProgressCharacter(string $char)
255
    {
256
        $this->progressChar = $char;
257
    }
258
259
    public function getProgressCharacter(): string
260
    {
261
        return $this->progressChar;
262
    }
263
264
    public function setFormat(string $format)
265
    {
266
        $this->format = null;
267
        $this->internalFormat = $format;
268
    }
269
270
    /**
271
     * Sets the redraw frequency.
272
     *
273
     * @param int|float $freq The frequency in steps
274
     */
275
    public function setRedrawFrequency(?int $freq)
276
    {
277
        $this->redrawFreq = null !== $freq ? max(1, $freq) : null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null !== $freq ? max(1, $freq) : null can also be of type double. However, the property $redrawFreq is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
278
    }
279
280
    public function minSecondsBetweenRedraws(float $seconds): void
281
    {
282
        $this->minSecondsBetweenRedraws = $seconds;
0 ignored issues
show
Documentation Bug introduced by
The property $minSecondsBetweenRedraws was declared of type integer, but $seconds is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
283
    }
284
285
    public function maxSecondsBetweenRedraws(float $seconds): void
286
    {
287
        $this->maxSecondsBetweenRedraws = $seconds;
0 ignored issues
show
Documentation Bug introduced by
The property $maxSecondsBetweenRedraws was declared of type integer, but $seconds is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
288
    }
289
290
    /**
291
     * Returns an iterator that will automatically update the progress bar when iterated.
292
     *
293
     * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
294
     */
295
    public function iterate(iterable $iterable, int $max = null): iterable
296
    {
297
        $this->start($max ?? (is_countable($iterable) ? \count($iterable) : 0));
298
299
        foreach ($iterable as $key => $value) {
300
            yield $key => $value;
301
302
            $this->advance();
303
        }
304
305
        $this->finish();
306
    }
307
308
    /**
309
     * Starts the progress output.
310
     *
311
     * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged
312
     */
313
    public function start(int $max = null)
314
    {
315
        $this->startTime = time();
316
        $this->step = 0;
317
        $this->percent = 0.0;
318
319
        if (null !== $max) {
320
            $this->setMaxSteps($max);
321
        }
322
323
        $this->display();
324
    }
325
326
    /**
327
     * Advances the progress output X steps.
328
     *
329
     * @param int $step Number of steps to advance
330
     */
331
    public function advance(int $step = 1)
332
    {
333
        $this->setProgress($this->step + $step);
334
    }
335
336
    /**
337
     * Sets whether to overwrite the progressbar, false for new line.
338
     */
339
    public function setOverwrite(bool $overwrite)
340
    {
341
        $this->overwrite = $overwrite;
342
    }
343
344
    public function setProgress(int $step)
345
    {
346
        if ($this->max && $step > $this->max) {
347
            $this->max = $step;
348
        } elseif ($step < 0) {
349
            $step = 0;
350
        }
351
352
        $redrawFreq = $this->redrawFreq ?? (($this->max ?: 10) / 10);
353
        $prevPeriod = (int) ($this->step / $redrawFreq);
354
        $currPeriod = (int) ($step / $redrawFreq);
355
        $this->step = $step;
0 ignored issues
show
Documentation Bug introduced by
It seems like $step can also be of type double. However, the property $step is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
356
        $this->percent = $this->max ? (float) $this->step / $this->max : 0;
357
        $timeInterval = microtime(true) - $this->lastWriteTime;
358
359
        // Draw regardless of other limits
360
        if ($this->max === $step) {
361
            $this->display();
362
363
            return;
364
        }
365
366
        // Throttling
367
        if ($timeInterval < $this->minSecondsBetweenRedraws) {
368
            return;
369
        }
370
371
        // Draw each step period, but not too late
372
        if ($prevPeriod !== $currPeriod || $timeInterval >= $this->maxSecondsBetweenRedraws) {
373
            $this->display();
374
        }
375
    }
376
377
    public function setMaxSteps(int $max)
378
    {
379
        $this->format = null;
380
        $this->max = max(0, $max);
381
        $this->stepWidth = $this->max ? Helper::strlen((string) $this->max) : 4;
382
    }
383
384
    /**
385
     * Finishes the progress output.
386
     */
387
    public function finish(): void
388
    {
389
        if (!$this->max) {
390
            $this->max = $this->step;
391
        }
392
393
        if ($this->step === $this->max && !$this->overwrite) {
394
            // prevent double 100% output
395
            return;
396
        }
397
398
        $this->setProgress($this->max);
399
    }
400
401
    /**
402
     * Outputs the current progress string.
403
     */
404
    public function display(): void
405
    {
406
        if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) {
407
            return;
408
        }
409
410
        if (null === $this->format) {
411
            $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat());
412
        }
413
414
        $this->overwrite($this->buildLine());
415
    }
416
417
    /**
418
     * Removes the progress bar from the current line.
419
     *
420
     * This is useful if you wish to write some output
421
     * while a progress bar is running.
422
     * Call display() to show the progress bar again.
423
     */
424
    public function clear(): void
425
    {
426
        if (!$this->overwrite) {
427
            return;
428
        }
429
430
        if (null === $this->format) {
431
            $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat());
432
        }
433
434
        $this->overwrite('');
435
    }
436
437
    private function setRealFormat(string $format)
438
    {
439
        // try to use the _nomax variant if available
440
        if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) {
441
            $this->format = self::getFormatDefinition($format.'_nomax');
442
        } elseif (null !== self::getFormatDefinition($format)) {
443
            $this->format = self::getFormatDefinition($format);
444
        } else {
445
            $this->format = $format;
446
        }
447
448
        $this->formatLineCount = substr_count($this->format, "\n");
449
    }
450
451
    /**
452
     * Overwrites a previous message to the output.
453
     */
454
    private function overwrite(string $message): void
455
    {
456
        if ($this->previousMessage === $message) {
457
            return;
458
        }
459
460
        $originalMessage = $message;
461
462
        if ($this->overwrite) {
463
            if (null !== $this->previousMessage) {
464
                if ($this->output instanceof ConsoleSectionOutput) {
465
                    $lines = floor(Helper::strlen($message) / $this->terminal->getWidth()) + $this->formatLineCount + 1;
466
                    $this->output->clear($lines);
467
                } else {
468
                    if ($this->formatLineCount > 0) {
469
                        $this->cursor->moveUp($this->formatLineCount);
470
                    }
471
472
                    $this->cursor->moveToColumn(1);
473
                    $this->cursor->clearLine();
474
                }
475
            }
476
        } elseif ($this->step > 0) {
477
            $message = PHP_EOL.$message;
478
        }
479
480
        $this->previousMessage = $originalMessage;
481
        $this->lastWriteTime = microtime(true);
482
483
        $this->output->write($message);
484
        ++$this->writeCount;
485
    }
486
487
    private function determineBestFormat(): string
488
    {
489
        switch ($this->output->getVerbosity()) {
490
            // OutputInterface::VERBOSITY_QUIET: display is disabled anyway
491
            case OutputInterface::VERBOSITY_VERBOSE:
492
                return $this->max ? 'verbose' : 'verbose_nomax';
493
            case OutputInterface::VERBOSITY_VERY_VERBOSE:
494
                return $this->max ? 'very_verbose' : 'very_verbose_nomax';
495
            case OutputInterface::VERBOSITY_DEBUG:
496
                return $this->max ? 'debug' : 'debug_nomax';
497
            default:
498
                return $this->max ? 'normal' : 'normal_nomax';
499
        }
500
    }
501
502
    private static function initPlaceholderFormatters(): array
503
    {
504
        return [
505
            'bar' => function (self $bar, OutputInterface $output) {
506
                $completeBars = $bar->getBarOffset();
507
                $display = str_repeat($bar->getBarCharacter(), $completeBars);
508
                if ($completeBars < $bar->getBarWidth()) {
509
                    $emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter());
510
                    $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars);
511
                }
512
513
                return $display;
514
            },
515
            'elapsed' => function (self $bar) {
516
                return Helper::formatTime(time() - $bar->getStartTime());
517
            },
518
            'remaining' => function (self $bar) {
519
                if (!$bar->getMaxSteps()) {
520
                    throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.');
521
                }
522
523
                return Helper::formatTime($bar->getRemaining());
524
            },
525
            'estimated' => function (self $bar) {
526
                if (!$bar->getMaxSteps()) {
527
                    throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.');
528
                }
529
530
                return Helper::formatTime($bar->getEstimated());
531
            },
532
            'memory' => function (self $bar) {
0 ignored issues
show
Unused Code introduced by
The parameter $bar is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
533
                return Helper::formatMemory(memory_get_usage(true));
534
            },
535
            'current' => function (self $bar) {
536
                return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', STR_PAD_LEFT);
537
            },
538
            'max' => function (self $bar) {
539
                return $bar->getMaxSteps();
540
            },
541
            'percent' => function (self $bar) {
542
                return floor($bar->getProgressPercent() * 100);
543
            },
544
        ];
545
    }
546
547
    private static function initFormats(): array
548
    {
549
        return [
550
            'normal' => ' %current%/%max% [%bar%] %percent:3s%%',
551
            'normal_nomax' => ' %current% [%bar%]',
552
553
            'verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%',
554
            'verbose_nomax' => ' %current% [%bar%] %elapsed:6s%',
555
556
            'very_verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%',
557
            'very_verbose_nomax' => ' %current% [%bar%] %elapsed:6s%',
558
559
            'debug' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%',
560
            'debug_nomax' => ' %current% [%bar%] %elapsed:6s% %memory:6s%',
561
        ];
562
    }
563
564
    private function buildLine(): string
565
    {
566
        $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i";
567
        $callback = function ($matches) {
568
            if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) {
569
                $text = $formatter($this, $this->output);
570
            } elseif (isset($this->messages[$matches[1]])) {
571
                $text = $this->messages[$matches[1]];
572
            } else {
573
                return $matches[0];
574
            }
575
576
            if (isset($matches[2])) {
577
                $text = sprintf('%'.$matches[2], $text);
578
            }
579
580
            return $text;
581
        };
582
        $line = preg_replace_callback($regex, $callback, $this->format);
583
584
        // gets string length for each sub line with multiline format
585
        $linesLength = array_map(function ($subLine) {
586
            return Helper::strlenWithoutDecoration($this->output->getFormatter(), rtrim($subLine, "\r"));
587
        }, explode("\n", $line));
588
589
        $linesWidth = max($linesLength);
590
591
        $terminalWidth = $this->terminal->getWidth();
592
        if ($linesWidth <= $terminalWidth) {
593
            return $line;
594
        }
595
596
        $this->setBarWidth($this->barWidth - $linesWidth + $terminalWidth);
597
598
        return preg_replace_callback($regex, $callback, $this->format);
599
    }
600
}
601