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

Snooze Laravel Package

thomasjohnkane/snooze

Snooze is a Laravel package for temporarily pausing scheduled commands, letting you snooze automated tasks for a set period and resume them later without editing your scheduler. Useful for maintenance windows and ad-hoc control.

View on GitHub
Deep Wiki
Context7

Getting Started

Snooze (thomasjohnkane/snooze) persists future notifications against an Eloquent model and fires them on a schedule, so you never have to build one-off "reminder" tables, jobs, or cron entries yourself.

Minimal path:

  1. composer require thomasjohnkane/snooze — 2.9.1 adds Laravel 13 support (^13.0 added to the constraint). No API changes from 2.9.0, so upgrading on Laravel 9–12 is a safe patch bump.
  2. Publish and run the migration: php artisan vendor:publish --provider="Thomasjohnkane\Snooze\SnoozeServiceProvider" then php artisan migrate. This creates the scheduled_notifications table (stored FQCN, morph target, send_at, sent_at, params payload).
  3. Add the trait to the model that owns the reminders: use Thomasjohnkane\Snooze\Traits\Snoozeable; on User.
  4. Schedule the sender every minute. Laravel 11/12/13: in routes/console.php → Schedule::command('snooze:send')->everyMinute()->withoutOverlapping();. On Laravel ≤10: $schedule->command('snooze:send')->everyMinute() in Kernel::schedule().

First use case — a trial-expiry nudge:

$user->snooze(TrialEndingReminder::class, now()->addDays(14));

Where to look first when something is off, in this order:

  • vendor/thomasjohnkane/snooze/src/ScheduledNotification.php — the persisted model, its casts, scopes and cancel() / sendNow() helpers.
  • vendor/thomasjohnkane/snooze/src/Commands/ — the snooze:send command, so you know exactly which query gates what goes out.
  • config/snooze.php (published) — queue/connection and retention knobs.
  • The README's Passing Parameters section, since that contract (constructor args vs. a params payload) is the one thing version-to-version readers get wrong.

Implementation Patterns

Attach the trait narrowly. Put Snoozeable only on models that actually own reminders (User, Subscription, Invoice). The trait gives you a scheduledNotifications morph-many relation, which is your listing/cancellation surface — don't query the ScheduledNotification model directly in app code unless you're building an admin view.

Keep the domain record as the source of truth for "what to cancel". snooze() returns the created ScheduledNotification. Persist its id on your own table (e.g. subscriptions.trial_reminder_id) instead of later guessing with where('notification', ...). Cancelling becomes ScheduledNotification::find($id)?->cancel().

Treat the notification as rebuilt-at-send-time. The row only stores a class name plus a serialized params payload; the notification is re-instantiated from scratch when the command runs. So the constructor signature is the contract:

$user->snooze(InvoiceOverdue::class, now()->addDays(3), ['invoice_id' => $invoice->id]);

class InvoiceOverdue extends Notification implements ShouldQueue
{
    public function __construct(public int $invoice_id) {}
    public function toMail($notifiable) { /* re-query here */ }
}

Pass scalars and IDs; re-query the real models inside toMail()/toArray(). Anything that isn't JSON-safe (a model instance, a closure, a relation) will either be flattened to an ID by the cast or blow up on hydrate.

Idempotency by domain key. Retried form submissions and queued listeners love to double-schedule. Either guard in the command that schedules (if ($subscription->trial_reminder_id) return;) or key on [model_type, model_id, notification, send_at] with updateOrCreate on the ScheduledNotification model, plus a unique index on that tuple.

Queueing is your concern, not the package's. snooze:send picks up due rows and dispatches the notification through Laravel's normal pipeline. If your notification implements ShouldQueue, the work happens on your queue; if not, it sends inline inside the scheduled command and one slow mailer stalls the whole batch. Always pair ->withoutOverlapping() with ->runInBackground() on the schedule entry, and set the snooze config's queue connection/name if reminders should live on a dedicated queue.

Testing. Reminders only exist as rows until the command runs, so assert on both layers:

Notification::fake();
$user->snooze(TrialEndingReminder::class, now()->addDays(14));
$this->travel(15)->days();
$this->artisan('snooze:send')->assertSuccessful();
Notification::assertSentTo($user, TrialEndingReminder::class);

Notification::fake() alone asserts nothing before snooze:send runs — the notification is never dispatched until then.

Integration tip: scheduling without a model. For notifiables that aren't Eloquent models (or are handled by an external service), use the package's static facade helper (Snooze::schedule(NotificationClass::class, $notifiable, $sendAt, $params)) rather than attaching the trait. Same storage, same command, no morph target.

Laravel 13 readiness. Nothing to change beyond the constraint; if your app is on 13, re-run the migration publish check to be sure you aren't pinned to a stale migration stub from an old install.

Gotchas and Tips

  • A silent scheduler is the #1 cause of "notifications aren't going out." snooze:send must actually run every minute. Locally that's php artisan schedule:work (not schedule:run once). In production verify schedule:run is in cron and that the command isn't being swallowed by withoutOverlapping after a crashed run (clear the mutex: schedule:clear-cache).
  • Timezone drift. The send_at column is a naive datetime and the query compares against now() in the app timezone. Set app.timezone to UTC, convert user-facing times with ->setTimezone() before display only, and never store a Carbon in a local zone. If you switched DBs (MySQL → Postgres), re-check the datetime column semantics.
  • Changing a notification's namespace breaks queued history. Rows store the FQCN verbatim. Renaming App\Notifications\TrialEnding leaves old rows pointing at a class that no longer exists — the command will error or skip. Migrate the scheduled_notifications.notification column in the same deploy, or keep a class_alias.
  • Deleting the parent leaves orphans. The morph relation isn't cascaded by default. Add a cleanup job (ScheduledNotification::where('send_at', '<', now()->subMonth())->delete() for sent/abandoned rows) or a whereMorphedTo-friendly FK with onDelete('cascade') on the published migration.
  • Debugging a single reminder: php artisan snooze:send runs the same query as the schedule, so you can force it. To inspect the row, ScheduledNotification::find($id) shows send_at as a Carbon plus the raw params — the params are the usual culprit when hydration fails.
  • Watch for sent_at vs. deletion. Depending on config, processed rows are either marked or pruned; don't build audit reports on them without pinning the retention setting. Your own domain log is a safer audit trail.
  • Extension point: if you need extra columns (tenant id, correlation id), publish the migration, add them, and bind your own subclass of ScheduledNotification so cancel()/scopes stay consistent — the package resolves the model through the container.
  • snooze() throws on an invalid class. It validates that the argument is a real Notification class at schedule time, so a typo fails loudly in the request rather than silently at 3 AM — good. But it won't validate that your constructor matches the params array; that mismatch surfaces only at send time, which is why a test that travels past send_at is worth writing for each snooze type you add.
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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment