Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 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\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 <fabien@symfony.com>
25
 * @author Chris Jones <leeked@gmail.com>
26
 */
27
final class ProgressBar
28
{
29
    public const FORMAT_VERBOSE = 'verbose';
30
    public const FORMAT_VERY_VERBOSE = 'very_verbose';
31
    public const FORMAT_DEBUG = 'debug';
32
    public const FORMAT_NORMAL = 'normal';
33
 
34
    private const FORMAT_VERBOSE_NOMAX = 'verbose_nomax';
35
    private const FORMAT_VERY_VERBOSE_NOMAX = 'very_verbose_nomax';
36
    private const FORMAT_DEBUG_NOMAX = 'debug_nomax';
37
    private const FORMAT_NORMAL_NOMAX = 'normal_nomax';
38
 
39
    private int $barWidth = 28;
40
    private string $barChar;
41
    private string $emptyBarChar = '-';
42
    private string $progressChar = '>';
43
    private ?string $format = null;
44
    private ?string $internalFormat = null;
45
    private ?int $redrawFreq = 1;
46
    private int $writeCount = 0;
47
    private float $lastWriteTime = 0;
48
    private float $minSecondsBetweenRedraws = 0;
49
    private float $maxSecondsBetweenRedraws = 1;
50
    private OutputInterface $output;
51
    private int $step = 0;
52
    private int $startingStep = 0;
53
    private ?int $max = null;
54
    private int $startTime;
55
    private int $stepWidth;
56
    private float $percent = 0.0;
57
    private array $messages = [];
58
    private bool $overwrite = true;
59
    private Terminal $terminal;
60
    private ?string $previousMessage = null;
61
    private Cursor $cursor;
62
 
63
    private static array $formatters;
64
    private static array $formats;
65
 
66
    /**
67
     * @param int $max Maximum steps (0 if unknown)
68
     */
69
    public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 1 / 25)
70
    {
71
        if ($output instanceof ConsoleOutputInterface) {
72
            $output = $output->getErrorOutput();
73
        }
74
 
75
        $this->output = $output;
76
        $this->setMaxSteps($max);
77
        $this->terminal = new Terminal();
78
 
79
        if (0 < $minSecondsBetweenRedraws) {
80
            $this->redrawFreq = null;
81
            $this->minSecondsBetweenRedraws = $minSecondsBetweenRedraws;
82
        }
83
 
84
        if (!$this->output->isDecorated()) {
85
            // disable overwrite when output does not support ANSI codes.
86
            $this->overwrite = false;
87
 
88
            // set a reasonable redraw frequency so output isn't flooded
89
            $this->redrawFreq = null;
90
        }
91
 
92
        $this->startTime = time();
93
        $this->cursor = new Cursor($output);
94
    }
95
 
96
    /**
97
     * Sets a placeholder formatter for a given name.
98
     *
99
     * This method also allow you to override an existing placeholder.
100
     *
101
     * @param string   $name     The placeholder name (including the delimiter char like %)
102
     * @param callable $callable A PHP callable
103
     */
104
    public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void
105
    {
106
        self::$formatters ??= self::initPlaceholderFormatters();
107
 
108
        self::$formatters[$name] = $callable;
109
    }
110
 
111
    /**
112
     * Gets the placeholder formatter for a given name.
113
     *
114
     * @param string $name The placeholder name (including the delimiter char like %)
115
     */
116
    public static function getPlaceholderFormatterDefinition(string $name): ?callable
117
    {
118
        self::$formatters ??= self::initPlaceholderFormatters();
119
 
120
        return self::$formatters[$name] ?? null;
121
    }
122
 
123
    /**
124
     * Sets a format for a given name.
125
     *
126
     * This method also allow you to override an existing format.
127
     *
128
     * @param string $name   The format name
129
     * @param string $format A format string
130
     */
131
    public static function setFormatDefinition(string $name, string $format): void
132
    {
133
        self::$formats ??= self::initFormats();
134
 
135
        self::$formats[$name] = $format;
136
    }
137
 
138
    /**
139
     * Gets the format for a given name.
140
     *
141
     * @param string $name The format name
142
     */
143
    public static function getFormatDefinition(string $name): ?string
144
    {
145
        self::$formats ??= self::initFormats();
146
 
147
        return self::$formats[$name] ?? null;
148
    }
149
 
150
    /**
151
     * Associates a text with a named placeholder.
152
     *
153
     * The text is displayed when the progress bar is rendered but only
154
     * when the corresponding placeholder is part of the custom format line
155
     * (by wrapping the name with %).
156
     *
157
     * @param string $message The text to associate with the placeholder
158
     * @param string $name    The name of the placeholder
159
     */
160
    public function setMessage(string $message, string $name = 'message')
161
    {
162
        $this->messages[$name] = $message;
163
    }
164
 
165
    public function getMessage(string $name = 'message')
166
    {
167
        return $this->messages[$name];
168
    }
169
 
170
    public function getStartTime(): int
171
    {
172
        return $this->startTime;
173
    }
174
 
175
    public function getMaxSteps(): int
176
    {
177
        return $this->max;
178
    }
179
 
180
    public function getProgress(): int
181
    {
182
        return $this->step;
183
    }
184
 
185
    private function getStepWidth(): int
186
    {
187
        return $this->stepWidth;
188
    }
189
 
190
    public function getProgressPercent(): float
191
    {
192
        return $this->percent;
193
    }
194
 
195
    public function getBarOffset(): float
196
    {
197
        return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth);
198
    }
199
 
200
    public function getEstimated(): float
201
    {
202
        if (0 === $this->step || $this->step === $this->startingStep) {
203
            return 0;
204
        }
205
 
206
        return round((time() - $this->startTime) / ($this->step - $this->startingStep) * $this->max);
207
    }
208
 
209
    public function getRemaining(): float
210
    {
211
        if (!$this->step) {
212
            return 0;
213
        }
214
 
215
        return round((time() - $this->startTime) / ($this->step - $this->startingStep) * ($this->max - $this->step));
216
    }
217
 
218
    public function setBarWidth(int $size)
219
    {
220
        $this->barWidth = max(1, $size);
221
    }
222
 
223
    public function getBarWidth(): int
224
    {
225
        return $this->barWidth;
226
    }
227
 
228
    public function setBarCharacter(string $char)
229
    {
230
        $this->barChar = $char;
231
    }
232
 
233
    public function getBarCharacter(): string
234
    {
235
        return $this->barChar ?? ($this->max ? '=' : $this->emptyBarChar);
236
    }
237
 
238
    public function setEmptyBarCharacter(string $char)
239
    {
240
        $this->emptyBarChar = $char;
241
    }
242
 
243
    public function getEmptyBarCharacter(): string
244
    {
245
        return $this->emptyBarChar;
246
    }
247
 
248
    public function setProgressCharacter(string $char)
249
    {
250
        $this->progressChar = $char;
251
    }
252
 
253
    public function getProgressCharacter(): string
254
    {
255
        return $this->progressChar;
256
    }
257
 
258
    public function setFormat(string $format)
259
    {
260
        $this->format = null;
261
        $this->internalFormat = $format;
262
    }
263
 
264
    /**
265
     * Sets the redraw frequency.
266
     *
267
     * @param int|null $freq The frequency in steps
268
     */
269
    public function setRedrawFrequency(?int $freq)
270
    {
271
        $this->redrawFreq = null !== $freq ? max(1, $freq) : null;
272
    }
273
 
274
    public function minSecondsBetweenRedraws(float $seconds): void
275
    {
276
        $this->minSecondsBetweenRedraws = $seconds;
277
    }
278
 
279
    public function maxSecondsBetweenRedraws(float $seconds): void
280
    {
281
        $this->maxSecondsBetweenRedraws = $seconds;
282
    }
283
 
284
    /**
285
     * Returns an iterator that will automatically update the progress bar when iterated.
286
     *
287
     * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable
288
     */
289
    public function iterate(iterable $iterable, int $max = null): iterable
290
    {
291
        $this->start($max ?? (is_countable($iterable) ? \count($iterable) : 0));
292
 
293
        foreach ($iterable as $key => $value) {
294
            yield $key => $value;
295
 
296
            $this->advance();
297
        }
298
 
299
        $this->finish();
300
    }
301
 
302
    /**
303
     * Starts the progress output.
304
     *
305
     * @param int|null $max     Number of steps to complete the bar (0 if indeterminate), null to leave unchanged
306
     * @param int      $startAt The starting point of the bar (useful e.g. when resuming a previously started bar)
307
     */
308
    public function start(int $max = null, int $startAt = 0): void
309
    {
310
        $this->startTime = time();
311
        $this->step = $startAt;
312
        $this->startingStep = $startAt;
313
 
314
        $startAt > 0 ? $this->setProgress($startAt) : $this->percent = 0.0;
315
 
316
        if (null !== $max) {
317
            $this->setMaxSteps($max);
318
        }
319
 
320
        $this->display();
321
    }
322
 
323
    /**
324
     * Advances the progress output X steps.
325
     *
326
     * @param int $step Number of steps to advance
327
     */
328
    public function advance(int $step = 1)
329
    {
330
        $this->setProgress($this->step + $step);
331
    }
332
 
333
    /**
334
     * Sets whether to overwrite the progressbar, false for new line.
335
     */
336
    public function setOverwrite(bool $overwrite)
337
    {
338
        $this->overwrite = $overwrite;
339
    }
340
 
341
    public function setProgress(int $step)
342
    {
343
        if ($this->max && $step > $this->max) {
344
            $this->max = $step;
345
        } elseif ($step < 0) {
346
            $step = 0;
347
        }
348
 
349
        $redrawFreq = $this->redrawFreq ?? (($this->max ?: 10) / 10);
350
        $prevPeriod = (int) ($this->step / $redrawFreq);
351
        $currPeriod = (int) ($step / $redrawFreq);
352
        $this->step = $step;
353
        $this->percent = $this->max ? (float) $this->step / $this->max : 0;
354
        $timeInterval = microtime(true) - $this->lastWriteTime;
355
 
356
        // Draw regardless of other limits
357
        if ($this->max === $step) {
358
            $this->display();
359
 
360
            return;
361
        }
362
 
363
        // Throttling
364
        if ($timeInterval < $this->minSecondsBetweenRedraws) {
365
            return;
366
        }
367
 
368
        // Draw each step period, but not too late
369
        if ($prevPeriod !== $currPeriod || $timeInterval >= $this->maxSecondsBetweenRedraws) {
370
            $this->display();
371
        }
372
    }
373
 
374
    public function setMaxSteps(int $max)
375
    {
376
        $this->format = null;
377
        $this->max = max(0, $max);
378
        $this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4;
379
    }
380
 
381
    /**
382
     * Finishes the progress output.
383
     */
384
    public function finish(): void
385
    {
386
        if (!$this->max) {
387
            $this->max = $this->step;
388
        }
389
 
390
        if ($this->step === $this->max && !$this->overwrite) {
391
            // prevent double 100% output
392
            return;
393
        }
394
 
395
        $this->setProgress($this->max);
396
    }
397
 
398
    /**
399
     * Outputs the current progress string.
400
     */
401
    public function display(): void
402
    {
403
        if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) {
404
            return;
405
        }
406
 
407
        if (null === $this->format) {
408
            $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat());
409
        }
410
 
411
        $this->overwrite($this->buildLine());
412
    }
413
 
414
    /**
415
     * Removes the progress bar from the current line.
416
     *
417
     * This is useful if you wish to write some output
418
     * while a progress bar is running.
419
     * Call display() to show the progress bar again.
420
     */
421
    public function clear(): void
422
    {
423
        if (!$this->overwrite) {
424
            return;
425
        }
426
 
427
        if (null === $this->format) {
428
            $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat());
429
        }
430
 
431
        $this->overwrite('');
432
    }
433
 
434
    private function setRealFormat(string $format)
435
    {
436
        // try to use the _nomax variant if available
437
        if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) {
438
            $this->format = self::getFormatDefinition($format.'_nomax');
439
        } elseif (null !== self::getFormatDefinition($format)) {
440
            $this->format = self::getFormatDefinition($format);
441
        } else {
442
            $this->format = $format;
443
        }
444
    }
445
 
446
    /**
447
     * Overwrites a previous message to the output.
448
     */
449
    private function overwrite(string $message): void
450
    {
451
        if ($this->previousMessage === $message) {
452
            return;
453
        }
454
 
455
        $originalMessage = $message;
456
 
457
        if ($this->overwrite) {
458
            if (null !== $this->previousMessage) {
459
                if ($this->output instanceof ConsoleSectionOutput) {
460
                    $messageLines = explode("\n", $this->previousMessage);
461
                    $lineCount = \count($messageLines);
462
                    foreach ($messageLines as $messageLine) {
463
                        $messageLineLength = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $messageLine));
464
                        if ($messageLineLength > $this->terminal->getWidth()) {
465
                            $lineCount += floor($messageLineLength / $this->terminal->getWidth());
466
                        }
467
                    }
468
                    $this->output->clear($lineCount);
469
                } else {
470
                    $lineCount = substr_count($this->previousMessage, "\n");
471
                    for ($i = 0; $i < $lineCount; ++$i) {
472
                        $this->cursor->moveToColumn(1);
473
                        $this->cursor->clearLine();
474
                        $this->cursor->moveUp();
475
                    }
476
 
477
                    $this->cursor->moveToColumn(1);
478
                    $this->cursor->clearLine();
479
                }
480
            }
481
        } elseif ($this->step > 0) {
482
            $message = \PHP_EOL.$message;
483
        }
484
 
485
        $this->previousMessage = $originalMessage;
486
        $this->lastWriteTime = microtime(true);
487
 
488
        $this->output->write($message);
489
        ++$this->writeCount;
490
    }
491
 
492
    private function determineBestFormat(): string
493
    {
494
        return match ($this->output->getVerbosity()) {
495
            // OutputInterface::VERBOSITY_QUIET: display is disabled anyway
496
            OutputInterface::VERBOSITY_VERBOSE => $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX,
497
            OutputInterface::VERBOSITY_VERY_VERBOSE => $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX,
498
            OutputInterface::VERBOSITY_DEBUG => $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX,
499
            default => $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX,
500
        };
501
    }
502
 
503
    private static function initPlaceholderFormatters(): array
504
    {
505
        return [
506
            'bar' => function (self $bar, OutputInterface $output) {
507
                $completeBars = $bar->getBarOffset();
508
                $display = str_repeat($bar->getBarCharacter(), $completeBars);
509
                if ($completeBars < $bar->getBarWidth()) {
510
                    $emptyBars = $bar->getBarWidth() - $completeBars - Helper::length(Helper::removeDecoration($output->getFormatter(), $bar->getProgressCharacter()));
511
                    $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars);
512
                }
513
 
514
                return $display;
515
            },
516
            'elapsed' => function (self $bar) {
517
                return Helper::formatTime(time() - $bar->getStartTime());
518
            },
519
            'remaining' => function (self $bar) {
520
                if (!$bar->getMaxSteps()) {
521
                    throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.');
522
                }
523
 
524
                return Helper::formatTime($bar->getRemaining());
525
            },
526
            'estimated' => function (self $bar) {
527
                if (!$bar->getMaxSteps()) {
528
                    throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.');
529
                }
530
 
531
                return Helper::formatTime($bar->getEstimated());
532
            },
533
            'memory' => function (self $bar) {
534
                return Helper::formatMemory(memory_get_usage(true));
535
            },
536
            'current' => function (self $bar) {
537
                return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT);
538
            },
539
            'max' => function (self $bar) {
540
                return $bar->getMaxSteps();
541
            },
542
            'percent' => function (self $bar) {
543
                return floor($bar->getProgressPercent() * 100);
544
            },
545
        ];
546
    }
547
 
548
    private static function initFormats(): array
549
    {
550
        return [
551
            self::FORMAT_NORMAL => ' %current%/%max% [%bar%] %percent:3s%%',
552
            self::FORMAT_NORMAL_NOMAX => ' %current% [%bar%]',
553
 
554
            self::FORMAT_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%',
555
            self::FORMAT_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%',
556
 
557
            self::FORMAT_VERY_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%',
558
            self::FORMAT_VERY_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%',
559
 
560
            self::FORMAT_DEBUG => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%',
561
            self::FORMAT_DEBUG_NOMAX => ' %current% [%bar%] %elapsed:6s% %memory:6s%',
562
        ];
563
    }
564
 
565
    private function buildLine(): string
566
    {
567
        \assert(null !== $this->format);
568
 
569
        $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i";
570
        $callback = function ($matches) {
571
            if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) {
572
                $text = $formatter($this, $this->output);
573
            } elseif (isset($this->messages[$matches[1]])) {
574
                $text = $this->messages[$matches[1]];
575
            } else {
576
                return $matches[0];
577
            }
578
 
579
            if (isset($matches[2])) {
580
                $text = sprintf('%'.$matches[2], $text);
581
            }
582
 
583
            return $text;
584
        };
585
        $line = preg_replace_callback($regex, $callback, $this->format);
586
 
587
        // gets string length for each sub line with multiline format
588
        $linesLength = array_map(function ($subLine) {
589
            return Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r")));
590
        }, explode("\n", $line));
591
 
592
        $linesWidth = max($linesLength);
593
 
594
        $terminalWidth = $this->terminal->getWidth();
595
        if ($linesWidth <= $terminalWidth) {
596
            return $line;
597
        }
598
 
599
        $this->setBarWidth($this->barWidth - $linesWidth + $terminalWidth);
600
 
601
        return preg_replace_callback($regex, $callback, $this->format);
602
    }
603
}