imtigger/laravel-job-status
Track the status of queued Laravel jobs. Add the JobStatus trait to a job, then query its state (queued, processing, completed, failed), progress, messages, and errors through a simple API. Statuses persist in cache or database, so you can show job progress in your UI and debug failures easily.
Install the package and publish its assets:
composer require <vendor>/<package>
php artisan vendor:publish --provider="<Vendor>\<Package>\<Package>ServiceProvider"
php artisan migrate
The publish step typically drops a config file (queue connection/table names, retention, enabled statuses) and a migration for the job status table. Read the config file first — it is the fastest map of what the package considers a "status transition" and which queue events it listens to.
Then look at, in this order:
JobStatusUpdater — the class responsible for translating Laravel queue events (or explicit calls) into status rows. As of 1.2.0 this is now an extension point, so treat it as the package's primary seam.First use case: put a long-running or business-critical job (invoice generation, report export, webhook delivery) on the tracked path, dispatch it, and watch the row move from queued → processing → completed. Confirm the row updates on a successful run and on a failed() run before you roll it out further. Two jobs are enough to validate; don't instrument the whole codebase up front.
Trait/base-class on jobs. The lowest-friction pattern is applying the package trait (or extending its base job) on jobs you care about. Keep this opt-in per job rather than global — high-volume, fire-and-forget jobs (cache warmers, analytics pings) create table churn without giving anyone value.
Customizing JobStatusUpdater (new in 1.2.0). This is the headline feature for day-to-day work. Extend the class and override the mapping methods (event → status, payload → metadata) when you need to:
ReportExportFailed) so notification logic lives in your app, not in the package.Bind the subclass in a service provider:
$this->app->bind(
\Vendor\Package\JobStatusUpdater::class,
\App\Queue\CustomJobStatusUpdater::class
);
If the package binds it as a singleton, override with $this->app->singleton(...) instead — binding a bind() over an existing singleton() silently does nothing. Check the provider before writing the binding.
Retry-aware handling. 1.2.0 marks a job as failed when it exhausts its retry count. Align your jobs' $tries, backoff, and retryUntil with what the package expects — the updater reads the attempt count from the queue event, so a job that self-limits retries in handle() rather than via $tries will report differently than you'd guess. If your jobs do manual retry logic ($this->release()), decide explicitly whether intermediate attempts should show as retrying or stay processing in your UI.
Deploy-safety with deleted classes. Jobs whose class no longer exists now fail safe instead of throwing during status update. This matters most right after a deploy that removes or renames a job class while old payloads are still in the queue. Pair it with a periodic query for statuses stuck in queued/processing past a threshold — the package will not resolve them for you, but it won't crash the worker either.
Querying status. Build one small read model around the table — a scoped query or repository — rather than spreading where('status', ...) across controllers. When the package's schema changes, that's the single file to fix.
Testing. Use Queue::fake() to assert dispatch, but use the real queue (sync or database driver) in at least one feature test to verify the status row actually transitions. A faked queue never fires the events the updater listens to, so status assertions silently pass on empty tables.
failed_jobs table is not replaced. This package tracks status; Laravel's own failed-job store and queue:failed tooling still work independently. Don't assume one implies the other, and don't build retry-from-dashboard logic on the status table alone.failed() method or in a listener on Laravel's JobFailed event, not in the status row.JobStatusUpdater subclass in register(), not boot(). If the package resolves the updater while registering queue listeners, a boot()-time binding arrives too late and you'll get the base class with no error.queue:work is firing events (Horizon and queue:work both work; a custom worker loop might not).processing rows. There is no fail-safe for a SIGKILL'd worker. Add a scheduled command that reconciles rows older than your longest job runtime.php artisan migrate
php artisan config:clear && php artisan queue:restart
queue:restart matters here: workers running the old code will still execute the pre-1.2.0 updater until they cycle.JobProcessing/JobProcessed, jobs dispatched inside a DB::transaction() may log statuses for work that gets rolled back. Dispatch after commit (afterCommit on the job or DB::afterCommit) where correctness matters.How can I help you explore Laravel packages today?