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

Progress Laravel Package

mrshanebarron/progress

A Laravel package for adding and displaying progress tracking in your applications. It provides a clean API for defining progress steps and helpers for rendering progress indicators, making it easy to visualize completion status in dashboards, workflows, and multi-step processes.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require mrshanebarron/progress
php artisan vendor:publish --tag=sb-progress-config

The v1.0.1 patch renamed the published config to the sb- prefix (config/sb-progress.php). If you are coming from v1.0.0, re-publish (--force) and diff before doing anything else — see Gotchas.

Where to look first

  1. config/sb-progress.php — storage driver, key/prefix, TTL, and any UI defaults. This tells you whether progress is kept in cache, session, or DB.
  2. The package service provider (src/*ServiceProvider.php) — confirms the config filename passed to mergeConfigFrom, the binding/alias for the tracker, and whether Blade components are auto-registered.
  3. src/ for the tracker class and its public methods (start/advance/finish/isComplete style API — confirm the exact names here before writing code around them).
  4. resources/views/ for any publishable Blade component, if you intend to use the UI layer rather than the tracker directly.

First use case: a queued import

Start with the smallest realistic flow — one queued job, one tracker, one Blade progress bar:

// app/Jobs/ImportContactsJob.php
public function handle(ProgressTracker $progress): void
{
    $rows = $this->readCsv($this->path);

    $progress->start($this->runId, total: $rows->count());

    foreach ($rows as $row) {
        $this->import($row);
        $progress->advance($this->runId);
    }

    $progress->finish($this->runId);
}

Then render it: dispatch the job, redirect, and have the view read the same run id from the tracker. If that loop works end to end, you have validated storage, persistence across the queue boundary, and the read path — the three things that break most often.

Implementation Patterns

Key every run by an id, never by "the current job"

Progress packages in this class are almost always keyed storage. Always namespace by something stable and unique per run — the job's UUID, an import record's id, or a ULID you generate at dispatch time and pass into the job. Sharing a single global key means two concurrent imports overwrite each other's counters and both report nonsense. Pass the id through the constructor and back to the front end in the redirect/session flash.

Write in the job, read in the controller

Keep the write side inside the worker and the read side inside a thin controller or Livewire action:

Route::get('/imports/{run}/progress', fn (string $run, ProgressTracker $progress) => [
    'done'     => $progress->completed($run),
    'total'    => $progress->total($run),
    'finished' => $progress->isComplete($run),
]);

Returning a small JSON payload keeps polling cheap and lets you swap the front end (Alpine poll, Livewire wire:poll, or a few seconds of setInterval) without touching the job.

Chunk your updates

Don't advance() once per row on a million-row import if the underlying store is a database table or Redis — you will spend more time writing progress than doing work. Advance every N items (100–1000) or by a computed delta (advance($run, $count)), and always do a final finish() so the bar lands on 100% rather than 99.7%.

Treat failure as an explicit state

The tracker tracks progress, not outcome. Wrap the job body and mark failure yourself so the UI can stop polling:

try {
    // ... work
    $progress->finish($runId);
} catch (\Throwable $e) {
    $progress->fail($runId, $e->getMessage()); // or set a status field
    throw $e;
}

Also handle the retry path: a retried job should reset or resume deterministically, not double-count. Decide up front whether your run id is stable across retries (resume) or regenerated (restart), and make the first line of handle() enforce it.

Gate multi-step flows on the completion check

The isComplete() check is the natural guard for chained workflows — don't advance to step 2 until step 1 reports done. Prefer checking the tracker over inferring from your own domain tables; it keeps the "is this still running?" question in one place and makes the UI and the orchestration agree.

Wrap it behind your own small service

Bind the package tracker behind an interface in your app and resolve your wrapper via the container. Two payoffs: tests can substitute an in-memory implementation, and if the package's API shifts (it's at v1.0.x, so it will), you change one adapter instead of grepping forty call sites.

Test the states, not the clock

In feature tests, drive the tracker directly to a mid-progress state and assert your controller's JSON shape and your Blade output. Assert the terminal states explicitly: 0 of N, N of N, and the "failed" branch. Those three are where UI regressions hide.

Gotchas and Tips

The v1.0.1 config rename is the sharpest edge

Renaming the published config file is a silent breaking change for anyone who published v1.0.0 assets. mergeConfigFrom() points at the new filename; the old published file is simply never loaded, so your overrides vanish and the package reverts to defaults — no exception, no warning. Symptoms are subtle: your configured cache store or key prefix stops applying and progress appears to reset or vanish.

Checklist when upgrading:

  • Run php artisan vendor:publish --force (or the provider tag), then diff the new config/sb-progress.php against your old file and port overrides forward.
  • grep -r your app, tests, and deploy scripts for the old config filename.
  • php artisan config:clear and confirm bootstrap/cache/config.php no longer holds the stale path (this bites hardest in deployments that cache config during build).
  • Pin the version in composer.json rather than floating ^1.0, and read the changelog on each bump — a patch-level config rename sets the precedent.

Cache-backed progress does not survive array in tests, and may not survive workers

If the default store is the cache, the array driver gives each process its own memory: your test asserts progress that the "job" wrote to a different array, and it passes for the wrong reason or fails inexplicably. Force a shared store (database, redis, file) for anything that crosses a process boundary, and set the test environment to something deterministic. Same trap in production with multiple queue workers and a per-process store.

Beware non-atomic increments

Read-modify-write counters lose updates under concurrency. If your store supports it, use the package's atomic path or Cache::increment()-style primitives; if it doesn't, serialize with a lock (Cache::lock) around the update or keep progress to a single writer. Two workers on one run id with a naive counter will drift low and never reach the total.

Divide-by-zero and the empty run

total: 0 makes percentage math explode or render NaN. Guard the percentage calculation in your view and decide what an empty run looks like in the UI (0/0 is "nothing to do", not "0%").

Polling: stop when done, expire eventually

A poll loop that never terminates keeps hitting your app after the job finishes. Have the endpoint return an explicit finished boolean and have the client clear its interval on it. Separately, set a sane TTL on stored progress so abandoned runs don't accumulate forever — check whether the package handles expiry or whether you need to clean up yourself.

Publish the UI assets deliberately

This is a UI component package, so expect a publishable Blade view and possibly a CSS dependency. If you publish the view, remember it's now yours to maintain across upgrades — vendor diffs on the next bump. If you have Tailwind configured with explicit content globs, ensure the package's view paths are included or the progress bar ships unstyled.

Debugging checklist

Progress "not updating"? In order: (1) is the same run id used on both sides — dump it in the job and the request; (2) is the config actually loaded — config('sb-progress.*') in tinker against what you expect; (3) same cache store in worker and web process — php artisan about / config('cache.default'); (4) is the write happening inside a DB transaction that later rolls back; (5) is the job even reaching the advance call — log at the start of handle(). Nine times in ten it's (1) or (3).

Extension points worth knowing

The config file is the intended seam: store driver, key prefix, and defaults are all meant to be set there rather than patched in vendor code. Beyond that, the container binding (or the facade's underlying class, if aliased) is the place to swap an implementation. Avoid subclassing the tracker directly — it's pre-2.0 and internal signatures aren't a contract; an adapter you own is the durable choice.

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