Package

Amrlotfy Laravel Chores

Batched, checkpointed, resumable data operations for Laravel. Write only the per-record logic.

Package

amrlotfy/laravel-chores

View on GitHub →

Laravel Chores

Batched, checkpointed, resumable data operations for Laravel.

Every Laravel app eventually needs to run something over a big table: backfill a new column, normalize legacy phone numbers, anonymize expired records. You write a quick loop, run it on the server... and then a deploy kills it at row 180,000 of 340,000 — with no idea where it stopped, whether re-running is safe, or which rows failed along the way.

Chores solves the plumbing once. You write only the per-record logic — batching, progress, checkpointing, safe resume, failure isolation, pause, and retry are handled for you. No Redis, no queue workers, no external services: state lives in your database, which makes it a natural fit for on-prem and air-gapped deployments too.

class NormalizePhoneNumbers extends Chore
{
public function collection(): Builder
{
return User::whereNotNull('phone')->where('phone', 'not like', '+%');
}
 
public function process($record): void
{
$record->update(['phone' => PhoneNumber::parse($record->phone, 'EG')->toE164()]);
}
}
$ php artisan chore:run NormalizePhoneNumbers
 
187240/341882 [▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░] 54%

Ctrl+C it. Deploy over it. Reboot the server. Then run the same command again — it resumes from the last checkpoint, and no row is processed twice.

Installation

composer require amrlotfy/laravel-chores
php artisan vendor:publish --tag=chores-migrations
php artisan migrate

Requires PHP 8.2+ and Laravel 12 or 13. Works on MySQL, PostgreSQL, and SQLite.

Quick start

Scaffold a chore:

php artisan make:chore BackfillInvoiceTotals

Fill in the two methods:

namespace App\Chores;
 
use AmrLotfy\Chores\Chore;
use App\Models\Invoice;
use Illuminate\Contracts\Database\Eloquent\Builder;
 
class BackfillInvoiceTotals extends Chore
{
// Optional tuning
public int $batchSize = 500;
public int $sleepBetweenBatches = 0; // seconds; throttle if the DB needs breathing room
 
/** Which records still need work. */
public function collection(): Builder
{
return Invoice::whereNull('total_cached');
}
 
/** Handle ONE record. Throwing marks it failed — the run continues. */
public function process($record): void
{
$record->update(['total_cached' => $record->lines()->sum('amount')]);
}
}

Run it:

php artisan chore:run BackfillInvoiceTotals

For recurring chores (retention purges, cleanups), compose with Laravel's scheduler:

$schedule->command('chore:run AnonymizeExpiredAppointments')->monthly();

The guarantee, stated honestly

  • The runner iterates by keyset pagination (WHERE id > cursor), so it is immune to the classic bug where processing rows out of your own WHERE clause makes offset-based chunking skip records.
  • Progress is checkpointed to the database after every batch. Kill the process any way you like — worst case, the rows of the single in-flight batch are re-examined on resume.
  • That means: exactly-once processing beyond the current batch, at-least-once within it. Write process() so that handling the same record twice is harmless (most update-style operations already are), and you're fully covered.
  • A record that throws is logged to chore_failures and never blocks the run. Retry just the failures later with chore:retry.

Commands

Command What it does
make:chore {name} Scaffold a chore class in app/Chores
chore:run {name} Run a chore — or resume its open run — with live progress
chore:list Available chores + recent run history
chore:pause {name} Pause a running chore at its next batch boundary (from another terminal)
chore:failures {name} List failed records with their exceptions
chore:retry {name} Re-process only the failed records

chore:run and chore:failures accept --json for machine-readable output — handy in CI pipelines and for AI coding agents. Exit codes: 0 clean, 1 completed with failures, 2 fatal.

Configuration

php artisan vendor:publish --tag=chores-config
Key Default Meaning
path app_path('Chores') Directory scanned for chore classes
namespace App\Chores Namespace matching that directory
default_batch_size 500 Used when a chore doesn't set $batchSize
table_names chore_runs / chore_failures Bookkeeping tables — rename before running the migration

Limitations (v1)

  • Iteration requires an orderable primary key: auto-increment integers and ULIDs work; random UUIDv4 keys are not supported.
  • Runs execute in the foreground artisan process (run inside tmux/screen, or via the scheduler). Queue-based execution is on the roadmap.
  • One worker per chore — no parallel processing yet.

Roadmap

  • Queued execution mode for very long runs
  • Self-hosted web dashboard (runs, progress, failures, retry button)
  • Parallel workers with range partitioning
  • --dry-run mode

Testing

composer test

The suite covers the guarantees above directly: crash-resume with exactly-once assertions, shrinking-predicate immunity, failure isolation, pause/SIGTERM at batch boundaries, ULID cursors, and counter-only mode.

Credits & license

Built by Amr Lotfy Saleh.

Inspired by Shopify's excellent maintenance_tasks for Rails.

MIT — see LICENSE.md.

Amrlotfy Laravel Chores Related Articles

Laravel Chores: Resumable Data Operations and Cleanups image

Laravel Chores: Resumable Data Operations and Cleanups

Read article
PhpStorm logo

PhpStorm

The go-to PHP IDE with extensive out-of-the-box support for Laravel and its ecosystem.

PhpStorm
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $9500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Acquaint Softtech logo

Acquaint Softtech

Acquaint Softtech offers AI-ready Laravel developers who onboard in 48 hours at $3000/Month with no lengthy sales process and a 100 percent money-back guarantee.

Acquaint Softtech
Pushsilver logo

Pushsilver

Pushsilver is simple invoicing for freelancers and small studios — send polished invoices, automate reminders, and get paid on time without the bloat.

Pushsilver
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift