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

Laravel Recurring Models Laravel Package

mohammedmanssour/laravel-recurring-models

Schedule recurring occurrences of any Eloquent model. Define a frequency and the package automatically creates the next instance, ideal for subscriptions, invoices, reminders, and other repeating records in Laravel. Supports flexible intervals.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish the migration.

composer require mohammedmanssour/laravel-recurring-models
php artisan vendor:publish --tag="recurring-models-migrations"
php artisan migrate

The package is auto-discovered. The migration creates the polymorphic recurrings table that stores recurrence rules and links generated occurrences back to the model that owns them.

Add the trait to the model that should recur.

use MohammedManssour\LaravelRecurringModels\Concerns\HasRecurring;

class Invoice extends Model
{
    use HasRecurring;
}

Schedule the runner. The package ships an artisan command that scans due recurrences and creates the next model instance. Register it in your scheduler (or routes/console.php in Laravel 11+):

$schedule->command('recurring:run')->everyMinute()->withoutOverlapping();

Your server still needs the standard * * * * * php artisan schedule:run cron entry — the package cannot fire recurrences on its own.

First use case: recurring invoices. Create an Invoice with recurring()->monthly()->at('09:00')->startingAt($invoice->starts_at), run php artisan recurring:run with Carbon::setTestNow(), and confirm a second invoice exists with the recurrence pointer populated. That end-to-end loop is the fastest way to validate your install.

Where to look first:

  1. config/recurring-models.php (if published) — model class, table name, and any scheduling knobs.
  2. The recurrings table schema — understand model_type/model_id, start/end dates, frequency columns, and the next-run timestamp.
  3. The HasRecurring trait — see which relations and scopes it adds (recurring, generated children, etc.).
  4. The recurring:run command source — know exactly what query it runs and how it picks due rows before you debug "why didn't it fire?"

Implementation Patterns

Template + occurrence pattern. Treat the model with recurrence rules as a template and every generated copy as an occurrence. Keep the template alive and query occurrences separately (via the generated child relation or recurring_id). Never let the template itself appear in user-facing invoice/task lists.

Centralize rules on the model. Rather than scattering ->monthly()->at(...) calls across controllers, expose a single method:

public function recurring()
{
    return $this->recurring
        ->monthly()
        ->at('09:00')
        ->startingAt($this->starts_at)
        ->endingAt($this->ends_at);
}

Now every code path that creates the model gets identical behavior, and you have one place to change when billing rules move.

Stamp copied data deliberately. Generated rows are duplicates of the template. Any field that must be unique per occurrence (invoice_number, due_date, period_start) should be overwritten in the model's creating/created event or a dedicated prepareOccurrence() method — the package cannot know your numbering scheme.

protected static function booted(): void
{
    static::creating(function (Invoice $invoice) {
        if ($invoice->recurring_id) {
            $invoice->number = app(InvoiceNumberer::class)->next();
        }
    });
}

Cancel by removing the rule, not the template. Deleting the parent cascades or orphans occurrences. Instead, delete/terminate its recurrence relation so the runner stops producing new rows while historical occurrences stay queryable.

Support "this occurrence only" edits. When a user edits a generated occurrence, write to that row directly. Because occurrences are ordinary Eloquent models, your existing update endpoints and policies work unchanged — the only special case is the template.

Batch and queue for scale. If you expect thousands of due rows per minute, wrap the runner in a queued job or chunk the recurrence query. The command itself should stay fast; the expensive part (copying, PDF generation, notifications) belongs in a created listener that dispatches a job.

Testing pattern. Because the command is time-driven, tests are simple with Carbon:

Carbon::setTestNow('2025-03-01 09:00:00');
$this->artisan('recurring:run');

$this->assertDatabaseCount('invoices', 2);
$this->assertDatabaseHas('invoices', ['recurring_id' => $template->id]);

Use travelTo()/travelBack() to test multi-period catch-up, monthly edge cases (Jan 31 → Feb 28), and end-date cutoffs.

Integrate through domain events. Listen to created on the generated model to send emails, fire webhooks, or notify Slack. This keeps the recurrence package unaware of your business logic and lets you replay events if generation was premature.

Gotchas and Tips

The scheduler is the #1 failure point. "Nothing recurs" almost always means schedule:run isn't running, the command is missing from the scheduler, or withoutOverlapping() plus a stuck run is silently skipping executions. Check php artisan schedule:list and the schedule:run logs before touching the package.

Double-generation is real. If the command runs twice (manual + scheduler), times out mid-run, or two servers share the cron, you'll create duplicate occurrences. Mitigations: ->withoutOverlapping(), ->onOneServer(), and a unique database index on (model_type, model_id, period_start) or your invoice number. Idempotency in the database is stronger than idempotency in PHP.

Timezones bite. Store timestamps in UTC and decide explicitly whether at('09:00') means the app timezone or the customer's. For recurring invoices billed per-client, apply the timezone per rule, or you'll send 3 a.m. emails to half your customer base.

Editing the template after occurrences exist does not retroactively change them. The recurrence rule governs future generations; already-created rows are frozen. If a customer changes from monthly to weekly, expect a transition period and decide whether to delete future occurrences manually.

Soft deletes and global scopes can hide the template from the runner. If your tenant/user global scope filters rows, the command may not see recurrences at all. Check whether the package's lookup applies scopes; if it does, run the command with withoutGlobalScopes() or design a dedicated recurrence model that isn't tenant-scoped.

Cascading deletes and foreign keys. recurring_id (or the package's equivalent) should be nullable and, depending on your choice, either cascade-delete occurrences or null them out. Test delete($template) early so you know which behavior you get before production shows it.

Mass assignment and replicate(). Generated rows come from a clone of the template, so $fillable and $guarded still matter. Also remember observers, accessors, and casts run on the clone — an encrypted or hashed cast may behave differently than expected in bulk generation.

Missing recurrences for missed windows. Most implementations generate the next occurrence, not every missed one. If the scheduler was down for three days, you may need a backfill command that replays due dates. Decide up front whether "catch up all missed periods" or "resume from now" is your contract.

Index for the runner. The recurring:run query filters on the next-run timestamp across the polymorphic table. Add an index on that column; without it, the command slows down as recurrences accumulate and starts overlapping.

Version your schema before production. Publish and commit the migration early. Editing the package's migration after it has run in staging is painful, and table/column names can shift between minor releases.

Extension points. Override the recurrence model or the runner command through the package config or container binding if you need custom frequency types (e.g., "every 2nd Tuesday"). Keep custom logic in a subclass rather than editing vendor files so upgrades stay safe.

Test the boundary dates. Jan 31 + monthly, Feb 29 in leap years, DST transitions on at('02:30') — the scheduler runs on wall-clock time, and these cases generate support tickets rather than exceptions. A handful of travelTo() tests will save you a postmortem.

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.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk