laminas/laminas-progressbar
Laminas ProgressBar renders progress bars for console and web output, with adapters for text terminals and JavaScript, showing percentage, ETA, elapsed time, and memory usage during long-running PHP tasks.
laminas/laminas-progressbar is a standalone progress-rendering component (formerly zend-progressbar) that ships console and browser adapters. As of 2.14.1 the project has been archived and abandoned (PR #45) — that is the headline of this release. No code changed, but the maintenance status did, and that should drive how you use it.
Where to look first
src/ProgressBar.php — the orchestrator (init(), advance(), update(), finish()).src/Adapter/Console.php — the only adapter that matters for a typical Laravel/Artisan workload.src/Adapter/JsPush.php and src/Adapter/JsPull.php — legacy browser-oriented adapters; almost never the right choice today.src/Adapter/AbstractAdapter.php — the extension point if you need a custom renderer.Adoption guidance for Laravel teams
Before writing any code, ask whether you need this package at all. Laravel's Artisan layer already wraps Symfony Console's progress bar via InteractsWithIO:
$this->output->progressStart($total);
foreach ($rows as $row) {
// ...
$this->output->progressAdvance();
}
$this->output->progressFinish();
That covers the overwhelming majority of day-to-day needs with a maintained dependency you already have. Reach for laminas-progressbar only when you are maintaining an existing Laminas/Zend application, when you genuinely need the JsPush/JsPull browser adapters, or when you need its multi-element rendering (bar + text + ETA-style counters) outside of a Symfony Console context.
First use case — installing into a legacy codebase and driving a long-running CLI loop:
composer require laminas/laminas-progressbar
use Laminas\ProgressBar\Adapter\Console;
use Laminas\ProgressBar\ProgressBar;
$adapter = new Console(['width' => 60, 'textWidth' => 30]);
$bar = new ProgressBar($adapter, 0, $total);
$bar->update(0, 'Starting…');
foreach ($rows as $row) {
process($row);
$bar->advance();
}
$bar->finish();
Pin the version and vendor the lockfile — an archived repository will not receive fixes.
Wrap the bar in a small service. Never sprinkle new ProgressBar(...) through your codebase. Inject an interface and bind either the real adapter or a no-op implementation per environment. This keeps tests clean and makes the eventual migration off the abandoned package a one-file change.
// app/Support/Progress/ProgressFactory.php
public function make(int $total, string $label = ''): ProgressBar
{
$adapter = app()->runningUnitTests() || ! $this->output->isDecorated()
? new Console(['width' => 0]) // or your own NullAdapter
: new Console(['width' => 80, 'textWidth' => 40]);
$bar = new ProgressBar($adapter, 0, $total);
$bar->setPrefix($label);
return $bar;
}
Command loop with guaranteed completion. Always finish() in a finally, otherwise the terminal cursor is left on the progress line and subsequent output interleaves badly.
$bar->update(0, 'Importing');
try {
User::query()->chunkById(500, function ($users) use ($bar) {
foreach ($users as $user) {
$this->import($user);
$bar->advance();
}
});
} finally {
$bar->finish();
}
Chunking is mandatory, not optional. Query with chunkById()/lazy() and advance inside the chunk callback. Loading 500k Eloquent models to feed a progress bar is the classic way to turn a cosmetic feature into an OOM.
Queue jobs. Progress does not survive process boundaries. If the work runs on workers, persist counters in the cache (Cache::increment("import:{$id}:done")) and render the bar in a separate polling command or an endpoint the UI hits. The bar is a view over a counter, never the counter itself.
Browser adapters. JsPush/JsPull require a session to persist state between requests and a view partial that emits the JS. They assume a full page-render flow and do not fit Livewire, Inertia, or SPA polling. For modern front ends, emit progress as JSON from a status endpoint and let the client draw it — do not bolt these adapters onto an API.
Suppress in non-interactive contexts. Guard on $this->output->isDecorated() and --quiet/--no-interaction. Cron jobs and CI logs should get plain lines, not carriage-return spam.
Bridge to the built-in bar. If you are progressively replacing the package, keep the call sites identical by mapping advance() → progressAdvance() and finish() → progressFinish() behind your wrapper, so the swap is invisible to callers.
new ProgressBar($adapter, 0, 0) and any update() with a zero maximum will produce divide-by-zero warnings or INF percentages. Short-circuit empty collections before constructing the bar.width, textWidth, elements, chars, prefix, and suffix are silently ignored when misspelled — you get a default-looking bar with no error. Verify visually once per adapter configuration rather than trusting the array.$this->line() mid-progress corrupts the carriage-return line. Buffer messages, or call $bar->finish() and restart a new bar per phase.update() vs advance(). advance() increments by one; update($current, $text) sets an absolute value. Mixing them within one loop produces nonsensical percentages — pick one and stay consistent.advance() was called exactly $total times. Rendered bar strings differ across terminal widths and will make tests flaky.JsPush/JsPull need a started session with a stable key. Long-running console commands in the same app that also start sessions can collide; namespace your keys.AbstractAdapter (or the adapter interface) rather than subclassing the concrete Console adapter — the concrete class is where styling assumptions live and it will not be maintained.ProgressBar or Artisan's progressStart()/progressAdvance() helpers. For browser progress, expose a status endpoint and poll it. Either replacement removes the abandoned dependency without changing the shape of your loop logic.How can I help you explore Laravel packages today?