Looking to hire Laravel developers? Try LaraJobs
This package is not available.

laravel-chores maintained by amrlotfy

Description
Batched, checkpointed, resumable data operations for Laravel. Write only the per-record logic.
Last update
2026/08/14 22:00 (dev-main)
License
Links
Downloads
449

Comments
comments powered by Disqus

Laravel Chores

Tests Latest Version on Packagist License

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.