Lines
98.33%
59 / 60
Methods
91.66%
11 / 12
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 1 | ||
| setFormat | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| setBarCharacters | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | ||
| setUpdateInterval | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| advance | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| update | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 3 | ||
| finish | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| display | 93.33% | 14 / 15 | 0.00% | 0 / 1 | 4.00 | ||
| getProgressBar | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 1 | ||
| getElapsedTime | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | ||
| getEstimatedTimeRemaining | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 2 | ||
| formatTime | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 3 | ||
| 14 | final class ProgressBar | |
| 15 | { | |
| 16 | /** | |
| 17 | * Total number of steps in the progress | |
| 18 | * | |
| 19 | * @var int | |
| 20 | */ | |
| 21 | private int $total; | |
| 22 | ||
| 23 | /** | |
| 24 | * Current position in the progress | |
| 25 | * | |
| 26 | * @var int | |
| 27 | */ | |
| 28 | private int $current = 0; | |
| 29 | ||
| 30 | /** | |
| 31 | * Width of the progress bar in characters | |
| 32 | * | |
| 33 | * @var int | |
| 34 | */ | |
| 35 | private int $barWidth = 50; | |
| 36 | ||
| 37 | /** | |
| 38 | * Timestamp when the progress bar was started | |
| 39 | * | |
| 40 | * @var float | |
| 41 | */ | |
| 42 | private float $startTime; | |
| 43 | ||
| 44 | /** | |
| 45 | * Format string for the progress bar output | |
| 46 | * | |
| 47 | * Available placeholders: | |
| 48 | * - {bar}: The actual progress bar | |
| 49 | * - {percent}: Percentage complete | |
| 50 | * - {current}: Current step | |
| 51 | * - {total}: Total steps | |
| 52 | * - {elapsed}: Elapsed time | |
| 53 | * - {eta}: Estimated time remaining | |
| 54 | * | |
| 55 | * @var string | |
| 56 | */ | |
| 57 | private string $format = '[{bar}] {percent}% ({current}/{total}) - {elapsed} - {eta} remaining'; | |
| 58 | ||
| 59 | /** | |
| 60 | * Characters used for the progress bar | |
| 61 | * | |
| 62 | * [0] => Character for completed portion | |
| 63 | * [1] => Character for incomplete portion | |
| 64 | * | |
| 65 | * @var array | |
| 66 | */ | |
| 67 | private array $barCharacters = ['█', '░']; | |
| 68 | ||
| 69 | /** | |
| 70 | * Timestamp of the last screen update | |
| 71 | * | |
| 72 | * @var float | |
| 73 | */ | |
| 74 | private float $lastUpdateTime = 0; | |
| 75 | ||
| 76 | /** | |
| 77 | * Minimum interval between screen updates in seconds | |
| 78 | * | |
| 79 | * @var float | |
| 80 | */ | |
| 81 | private float $updateInterval = 0.1; | |
| 82 | ||
| 83 | ||
| 84 | /** | |
| 85 | * Initialize the progress bar | |
| 86 | * | |
| 87 | * @param int $total Total number of items to process | |
| 88 | * @param int $barWidth Width of the progress bar in characters | |
| 89 | */ | |
| 90 | public function __construct(int $total, int $barWidth = 50) | |
| 91 | { | |
| 92 | $this->total = $total; | |
| 93 | $this->barWidth = $barWidth; | |
| 94 | $this->startTime = microtime(true); | |
| 95 | } | |
| 96 | ||
| 97 | /** | |
| 98 | * Set custom format for the progress bar | |
| 99 | * | |
| 100 | * Available placeholders: | |
| 101 | * - {bar}: The actual progress bar | |
| 102 | * - {percent}: Percentage complete | |
| 103 | * - {current}: Current step | |
| 104 | * - {total}: Total steps | |
| 105 | * - {elapsed}: Elapsed time | |
| 106 | * - {eta}: Estimated time remaining | |
| 107 | * | |
| 108 | * @param string $format Format string | |
| 109 | * @return self | |
| 110 | */ | |
| 111 | public function setFormat(string $format): self | |
| 112 | { | |
| 113 | $this->format = $format; | |
| 114 | return $this; | |
| 115 | } | |
| 116 | ||
| 117 | /** | |
| 118 | * Set custom characters for the progress bar | |
| 119 | * | |
| 120 | * @param array $chars Array with [complete_char, incomplete_char] | |
| 121 | * @return self | |
| 122 | */ | |
| 123 | public function setBarCharacters(array $chars): self | |
| 124 | { | |
| 125 | if (count($chars) === 2) { | |
| 126 | $this->barCharacters = $chars; | |
| 127 | } | |
| 128 | return $this; | |
| 129 | } | |
| 130 | ||
| 131 | /** | |
| 132 | * Set update interval to avoid too frequent screen updates | |
| 133 | * | |
| 134 | * @param float $seconds Minimum seconds between updates | |
| 135 | * @return self | |
| 136 | */ | |
| 137 | public function setUpdateInterval(float $seconds): self | |
| 138 | { | |
| 139 | $this->updateInterval = $seconds; | |
| 140 | return $this; | |
| 141 | } | |
| 142 | ||
| 143 | /** | |
| 144 | * Advance the progress bar by a specific step | |
| 145 | * | |
| 146 | * @param int $step Number of steps to advance | |
| 147 | * @return self | |
| 148 | */ | |
| 149 | public function advance(int $step = 1): self | |
| 150 | { | |
| 151 | $this->update($this->current + $step); | |
| 152 | return $this; | |
| 153 | } | |
| 154 | ||
| 155 | /** | |
| 156 | * Update the progress bar to a specific position | |
| 157 | * | |
| 158 | * @param int $current Current position | |
| 159 | * @return self | |
| 160 | */ | |
| 161 | public function update(int $current): self | |
| 162 | { | |
| 163 | $this->current = min(max(0, $current), $this->total); | |
| 164 | ||
| 165 | $now = microtime(true); | |
| 166 | if (($now - $this->lastUpdateTime) < $this->updateInterval && $this->current < $this->total) { | |
| 167 | return $this; | |
| 168 | } | |
| 169 | ||
| 170 | $this->lastUpdateTime = $now; | |
| 171 | $this->display(); | |
| 172 | ||
| 173 | return $this; | |
| 174 | } | |
| 175 | ||
| 176 | /** | |
| 177 | * Finish the progress bar and add a newline | |
| 178 | * | |
| 179 | * @return void | |
| 180 | */ | |
| 181 | public function finish(): void | |
| 182 | { | |
| 183 | $this->update($this->total); | |
| 184 | echo PHP_EOL; | |
| 185 | } | |
| 186 | ||
| 187 | /** | |
| 188 | * Display the progress bar | |
| 189 | * | |
| 190 | * @return void | |
| 191 | */ | |
| 192 | protected function display(): void | |
| 193 | { | |
| 194 | $termWidth = 80; | |
| 195 | if (function_exists('exec') && false !== @exec('tput cols 2>/dev/null', $output) && !empty($output[0])) { | |
| 196 | $termWidth = (int)$output[0]; | |
| 197 | } | |
| 198 | ||
| 199 | $clearLine = "\r" . str_repeat(' ', $termWidth) . "\r"; | |
| 200 | echo $clearLine; | |
| 201 | ||
| 202 | $percent = $this->current / $this->total; | |
| 203 | $bar = $this->getProgressBar($percent); | |
| 204 | $elapsedTime = $this->getElapsedTime(); | |
| 205 | $eta = $this->getEstimatedTimeRemaining($percent, $elapsedTime); | |
| 206 | ||
| 207 | $output = str_replace( | |
| 208 | ['{bar}', '{percent}', '{current}', '{total}', '{elapsed}', '{eta}'], | |
| 209 | [$bar, round($percent * 100), $this->current, $this->total, $elapsedTime, $eta], | |
| 210 | $this->format | |
| 211 | ); | |
| 212 | ||
| 213 | echo $output; | |
| 214 | } | |
| 215 | /** | |
| 216 | * Generate the actual progress bar string | |
| 217 | * | |
| 218 | * @param float $percent Percentage complete (0.0 to 1.0) | |
| 219 | * @return string | |
| 220 | */ | |
| 221 | protected function getProgressBar(float $percent): string | |
| 222 | { | |
| 223 | // Ensure percent is within valid range (0.0 to 1.0) | |
| 224 | $percent = max(0.0, min(1.0, $percent)); | |
| 225 | ||
| 226 | $completeBars = (int)floor($percent * $this->barWidth); | |
| 227 | $incompleteBars = max(0, $this->barWidth - $completeBars); | |
| 228 | ||
| 229 | return str_repeat($this->barCharacters[0], $completeBars) . | |
| 230 | str_repeat($this->barCharacters[1], $incompleteBars); | |
| 231 | } | |
| 232 | ||
| 233 | /** | |
| 234 | * Format elapsed time | |
| 235 | * | |
| 236 | * @return string | |
| 237 | */ | |
| 238 | protected function getElapsedTime(): string | |
| 239 | { | |
| 240 | $elapsed = microtime(true) - $this->startTime; | |
| 241 | return $this->formatTime($elapsed); | |
| 242 | } | |
| 243 | ||
| 244 | /** | |
| 245 | * Calculate and format estimated time remaining | |
| 246 | * | |
| 247 | * @param float $percent Percentage complete (0.0 to 1.0) | |
| 248 | * @param string $elapsedTime Formatted elapsed time string | |
| 249 | * @return string | |
| 250 | */ | |
| 251 | protected function getEstimatedTimeRemaining(float $percent, string $elapsedTime): string | |
| 252 | { | |
| 253 | if ($percent == 0) { | |
| 254 | return 'calculating...'; | |
| 255 | } | |
| 256 | ||
| 257 | $elapsed = microtime(true) - $this->startTime; | |
| 258 | $total = $elapsed / $percent; | |
| 259 | $remaining = $total - $elapsed; | |
| 260 | ||
| 261 | return $this->formatTime($remaining); | |
| 262 | } | |
| 263 | ||
| 264 | /** | |
| 265 | * Format time in human-readable format | |
| 266 | * | |
| 267 | * @param float $seconds Time in seconds | |
| 268 | * @return string | |
| 269 | */ | |
| 270 | protected function formatTime(float $seconds): string | |
| 271 | { | |
| 272 | if ($seconds < 60) { | |
| 273 | return sprintf("%.1fs", $seconds); | |
| 274 | } elseif ($seconds < 3600) { | |
| 275 | $minutes = (int)floor($seconds / 60); | |
| 276 | $remainingSeconds = fmod($seconds, 60.0); | |
| 277 | return sprintf("%dm %.0fs", $minutes, $remainingSeconds); | |
| 278 | } else { | |
| 279 | $hours = (int)floor($seconds / 3600); | |
| 280 | $remainingSeconds = fmod($seconds, 3600.0); | |
| 281 | $minutes = (int)floor($remainingSeconds / 60); | |
| 282 | $finalSeconds = fmod($remainingSeconds, 60.0); | |
| 283 | ||
| 284 | return sprintf("%dh %02dm %.0fs", $hours, $minutes, $finalSeconds); | |
| 285 | } | |
| 286 | } | |
| 287 | } |