PHPackages                             amrlotfy/laravel-chores - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. amrlotfy/laravel-chores

ActiveLibrary[Database &amp; ORM](/categories/database)

amrlotfy/laravel-chores
=======================

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

v0.1.0(today)00MITPHPPHP ^8.2CI passing

Since Aug 14Pushed todayCompare

[ Source](https://github.com/AmrLotfy/laravel-chores)[ Packagist](https://packagist.org/packages/amrlotfy/laravel-chores)[ RSS](/packages/amrlotfy-laravel-chores/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (5)Versions (2)Used By (0)

Laravel Chores
==============

[](#laravel-chores)

[![Tests](https://github.com/AmrLotfy/laravel-chores/actions/workflows/tests.yml/badge.svg)](https://github.com/AmrLotfy/laravel-chores/actions/workflows/tests.yml)[![Latest Version on Packagist](https://camo.githubusercontent.com/25a5160cbcd8a6d6e4d4dd395ea793b9cf63942b13842314f1d9f632a8069483/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f616d726c6f7466792f6c61726176656c2d63686f7265732e737667)](https://packagist.org/packages/amrlotfy/laravel-chores)[![License](https://camo.githubusercontent.com/946faf2a561b186fd6f983e1392a13265768a3d3832b8426bca0c1a7b3475c77/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f616d726c6f7466792f6c61726176656c2d63686f7265732e737667)](LICENSE.md)

**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
------------

[](#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
-----------

[](#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-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
--------

[](#commands)

CommandWhat 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
-------------

[](#configuration)

```
php artisan vendor:publish --tag=chores-config
```

KeyDefaultMeaning`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 migrationLimitations (v1)
----------------

[](#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
-------

[](#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
-------

[](#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 &amp; license
---------------------

[](#credits--license)

Built by [Amr Lotfy Saleh](https://www.linkedin.com/in/amr-lotfy-saleh/).

Inspired by Shopify's excellent [maintenance\_tasks](https://github.com/Shopify/maintenance_tasks) for Rails.

MIT — see [LICENSE.md](LICENSE.md).

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity35

Early-stage or recently created project

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/19728125?v=4)[Amr Lotfy](/maintainers/AmrLotfy)[@AmrLotfy](https://github.com/AmrLotfy)

---

Tags

artisanbackfillbatch-processingdata-migrationeloquentlaravelmaintenance-tasksphplaravelbatchmaintenancebackfilldata migration

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/amrlotfy-laravel-chores/health.svg)

```
[![Health](https://phpackages.com/badges/amrlotfy-laravel-chores/health.svg)](https://phpackages.com/packages/amrlotfy-laravel-chores)
```

###  Alternatives

[spatie/laravel-medialibrary

Associate files with Eloquent models

6.2k45.4M698](/packages/spatie-laravel-medialibrary)[laravel/ai

The official AI SDK for Laravel.

1.1k4.6M307](/packages/laravel-ai)[illuminate/queue

The Illuminate Queue package.

20433.0M1.8k](/packages/illuminate-queue)[mike-bronner/laravel-model-caching

Automatic caching for Eloquent models.

2.4k161.4k2](/packages/mike-bronner-laravel-model-caching)[psalm/plugin-laravel

Psalm plugin for Laravel

3345.4M354](/packages/psalm-plugin-laravel)[forjedio/inertia-table

Backend-driven dynamic tables for Laravel + Inertia.js

272.0k](/packages/forjedio-inertia-table)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
