Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Php Cli Progress Bar Laravel Package

jord-jd/php-cli-progress-bar

A lightweight PHP library for rendering progress bars in command-line scripts. Track the progress of loops and long-running tasks with customizable bars, percentage complete, and elapsed time, with no dependencies. Ideal for CLI tools, batch jobs, and console applications.

Deep Wiki
Context7

Getting Started

This is a plain-PHP, framework-agnostic progress bar. There is no service provider, no facade, and no config file — you composer require it and instantiate a class inside a script you already run from the terminal.

composer require jord-jd/php-cli-progress-bar

The first place to look is src/ — the whole library is one or two small classes, so reading the constructor signature and the render method takes about two minutes and tells you more than the README will. Confirm three things before writing any code: the exact namespace, whether the constructor takes a total step count, and which methods drive the bar (start() / advance() / stop() is the conventional shape for packages like this, but verify).

A minimal first use case:

<?php
require __DIR__ . '/vendor/autoload.php';

use JordJD\PhpCliProgressBar\ProgressBar;

$rows = range(1, 500);

$bar = new ProgressBar(count($rows));
$bar->start();

foreach ($rows as $row) {
    // do the actual work
    usleep(2000);
    $bar->advance();
}

$bar->stop();

That is the entire mental model: you know the total, you tick once per unit of work, you close the bar. Everything below is about doing that without it becoming a problem in a real codebase.

One scoping note up front: if you're inside a Laravel Artisan command, you already have $this->output->createProgressBar($total) from Symfony Console, which respects --quiet, handles verbosity, and needs no dependency. Reach for this package for standalone scripts, cron jobs, one-off ETL/import scripts, and seeders run outside the console component — or when you deliberately want a bar with a different look than Symfony's.

Implementation Patterns

Wrap it, don't call it scattered around. The moment you call new ProgressBar() in twelve scripts, swapping or removing it is a grep-and-pray exercise. Put a tiny interface in front of it:

interface Progress
{
    public function start(int $total): void;
    public function advance(int $step = 1): void;
    public function finish(): void;
}

final class NullProgress implements Progress
{
    public function start(int $total): void {}
    public function advance(int $step = 1): void {}
    public function finish(): void {}
}

Bind NullProgress when the script isn't attached to a TTY (CI, piped output, cron redirecting to a log file). This single decision removes the most common class of "why is my log file full of [=====> ]" complaints.

Decide what one tick means, and stick to it. For a chunked Eloquent import, ticking once per chunk makes the bar jump in visible steps and requires you to compute ceil($total / $chunkSize); ticking once per row is smoother but means calling advance() inside a tight loop. For a million rows, tick per batch and use the batch size as the step argument if the library supports one — or accept a coarser bar. Pick one and document it in a comment, because the "total" you pass to the constructor must match.

Get the total cheaply, or don't get it. A SELECT COUNT(*) on a huge table purely to draw a progress bar is often the slowest part of the script. Two acceptable options: run the count once and cache it, or restructure as an indeterminate bar if the library offers one. If it doesn't, a spinner or a simple "processed N items" line is more honest than a bar that lies about its remaining time.

Chunked work with early exit:

$total = Order::where('exported', false)->count(); // one cheap aggregate
$bar = new ProgressBar($total);
$bar->start();

Order::where('exported', false)
    ->chunkById(500, function ($orders) use ($bar) {
        foreach ($orders as $order) {
            $this->export($order);
            $bar->advance();
        }
    });

$bar->stop();

Note chunkById rather than chunk — mutating exported while paging by offset silently skips rows, and your bar will hit 100% while records remain. The progress bar makes that bug visible, which is a genuine benefit: bar finishes fast and the follow-up count() is still non-zero, so you know something is wrong.

Always finish in a finally. If the loop throws, the terminal is left mid-line and the exception trace is printed on top of the bar. $bar->stop() in finally gives you a newline before the stack trace:

$bar->start();
try {
    foreach ($items as $item) { $this->handle($item); $bar->advance(); }
} finally {
    $bar->stop();
}

Guard on context, once, at the entry point:

$progress = (PHP_SAPI === 'cli' && defined('STDOUT') && stream_isatty(STDOUT))
    ? $factory->makeBar()
    : new NullProgress();

This is more reliable than checking app()->runningInConsole() in Laravel, because a queued job also "runs in console" and should never render a bar. Progress bars belong to foreground, human-supervised execution — keep them out of jobs, listeners, and anything that might be retried.

Respect verbosity flags. If the script takes -q / --quiet, map it to NullProgress. Users redirecting output to a file will thank you.

Gotchas and Tips

License is GPL-3.0 — check this before you ship. GPL is copyleft. For an internal script or a dev-only tool this is usually a non-issue, but this is not MIT. If the application is proprietary and distributed, get a look from someone who knows your obligations before this becomes load-bearing. It is the single most consequential fact about this package.

It is a 15-star, single-maintainer library. Pin the version in composer.json (e.g. "jord-jd/php-cli-progress-bar": "^1.0") rather than tracking dev-main. The wrapper interface above is your insurance: if the package is abandoned in two years, you reimplement three methods.

Output buffering silently kills the bar. If anything upstream calls ob_start() — a framework bootstrap, a PHPUnit bootstrap, a profiler, some dd()-style helper — cursor-return and carriage-return sequences get swallowed and you'll see either nothing or one giant block of frames at the end. If the bar "doesn't work," this is the first thing to check. Add ob_get_level() to your debugging.

Don't render on every iteration for large loops. Each advance() that redraws is a fwrite plus escape sequences. Over 500k iterations that is measurable. Batch your advances (every 100 or 1000 items) or only redraw when a percentage point changes. Benchmark before optimizing, but know the hot path exists.

ANSI on Windows. Modern Windows Terminal and PowerShell 7 handle \r and ANSI fine; older cmd.exe and some CI runners may not, producing staircase output. The safe fallback is NullProgress or a plain percentage printed on its own line — check PHP_OS_FAMILY === 'Windows' if you support Windows dev machines.

Escape sequences in CI logs. Even if it renders, CI systems turn \r into either nothing or a wall of frames. Detect CI (CI or GITHUB_ACTIONS env var) and null the bar. Never let a pipeline's stdout be polluted by cursor control.

Subclassing is the realistic extension point. Small libraries like this usually expose width/format as constructor arguments or protected properties. Read the class before you hack around it — if the properties are protected and the class isn't final, subclass to change the bar characters or format string and keep the parent's render loop. If it's final or the properties are private, wrap and reimplement rather than patching. Do not edit vendor/ — that change vanishes on the next composer install.

Interrupt handling. On Ctrl-C the bar's line is left dirty. If the script is long-running, register a signal handler that finishes the bar and prints a newline before exiting. Without it, users see a half-drawn bar concatenated with their shell prompt.

Watch the total/actual mismatch. Most naive progress bar implementations compute percentage as current / total and will happily render above 100% or negative remaining time if you over-advance. If your batch sizes vary, clamp your own counters and advance in units you control, rather than trusting a count($results) you computed three queries ago.

Debugging checklist, in order: (1) is ob_get_level() zero? (2) is STDOUT actually a TTY? (3) does the constructor's total match the number of advance() calls? (4) are you calling stop()/finish() at all — some implementations buffer until closed.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings