Great question! What you’re describing is a classic notification batching pattern, and it’s a great way to improve scalability and user experience.
Here’s a step-by-step approach to implement this in Laravel (works for Laravel 10/11/12):
1. Database Design
You’ll need a notifications table (Laravel provides this if you use notifications), but you’ll want to add a sent (boolean) and maybe a batch_id (nullable) column to track which notifications have been included in a batch email.
Migration Example:
php artisan make:migration add_sent_and_batch_id_to_notifications_table --table=notifications
// database/migrations/xxxx_xx_xx_add_sent_and_batch_id_to_notifications_table.php
public function up()
{
Schema::table('notifications', function (Blueprint $table) {
$table->boolean('sent')->default(false);
$table->string('batch_id')->nullable();
});
}
2. Storing Notifications
When a chirp is created, store a notification for each user as usual, but do not send an email immediately.
// In your ChirpCreated event or observer
foreach (User::where('id', '!=', $chirp->user_id)->get() as $user) {
$user->notifications()->create([
'type' => ChirpNotification::class,
'data' => [
'chirp_id' => $chirp->id,
// ...other data
],
'sent' => false,
]);
}
3. Batching Logic
You need a scheduled job (artisan command or queued job) that:
- For each user, finds all unsent notifications.
- If the count >=
m, send an email with up tomnotifications, mark them as sent (and optionally assign abatch_id). - At the end of the day, send any remaining unsent notifications.
Example Command
php artisan make:command SendBatchedNotifications
// app/Console/Commands/SendBatchedNotifications.php
public function handle()
{
$batchSize = 25; // or config('notifications.batch_size')
$users = User::all();
foreach ($users as $user) {
$unsent = $user->notifications()
->where('sent', false)
->orderBy('created_at')
->take($batchSize)
->get();
if ($unsent->count() >= $batchSize) {
$this->sendBatch($user, $unsent);
}
}
}
protected function sendBatch($user, $notifications)
{
// Send email with notifications
Mail::to($user->email)->send(new BatchedChirpNotificationMail($notifications));
// Mark as sent
$ids = $notifications->pluck('id');
$user->notifications()->whereIn('id', $ids)->update([
'sent' => true,
'batch_id' => Str::uuid(),
]);
}
4. End-of-Day Batch
Schedule another command (or add logic to the above) to run at, say, 23:55, that sends all remaining unsent notifications for each user.
// In the same command, add:
foreach ($users as $user) {
$unsent = $user->notifications()->where('sent', false)->get();
if ($unsent->count() > 0) {
$this->sendBatch($user, $unsent);
}
}
5. Scheduling
Add to app/Console/Kernel.php:
protected function schedule(Schedule $schedule)
{
// Run every 10 minutes for regular batching
$schedule->command('notifications:batch')->everyTenMinutes();
// Run at end of day for leftovers
$schedule->command('notifications:batch --end-of-day')->dailyAt('23:55');
}
6. Mail Example
Create a mailable to display the batch:
php artisan make:mail BatchedChirpNotificationMail
// app/Mail/BatchedChirpNotificationMail.php
public function __construct($notifications)
{
$this->notifications = $notifications;
}
public function build()
{
return $this->view('emails.batched_chirps')
->with(['notifications' => $this->notifications]);
}
7. Summary
- Store notifications as usual, but don’t email immediately.
- Use a scheduled command to batch and send notifications per user.
- At the end of the day, send any remaining notifications.
- Mark notifications as sent to avoid duplicates.
Extra Tips
- You can use Laravel’s built-in notification system, but you’ll need to override the default
toMailmethod to prevent immediate sending. - Consider using queues for sending emails to avoid blocking.
- Make the batch size configurable.
Let me know if you need a full code example for any part of this!