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
14final 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}