Currently Available: Need a skilled Software Developer for your next project?
Categories
Laravel

Sending Custom Notification Emails in Laravel

Welcoming new users, celebrating their achievements, or notifying them about important events: sending well-timed emails can significantly improve user engagement.

I'll show you how to create and send custom notification emails in Laravel.

Examples include:

  • Welcome emails after email verification
  • Achievement notifications (first project created, milestone reached)
  • Activity digests (weekly summaries)
  • Status update notifications
  • Expiration/renewal reminders

Understanding Custom Notifications in Laravel

Before we dive into specific examples, let's understand how Laravel's notification system works and how we can use it to send emails.

The Basic Structure

A Laravel notification consists of three main components:

  1. A notification class that defines:

    • What channels to use (email, SMS, Slack, etc.)
    • The content of your message
    • Any customization or logic

    This class acts as the central point for your notification logic. It determines how the notification should be delivered and what content should be included.

  2. An email template that controls:

    • The visual layout of your email
    • How your content is displayed
    • Styling and branding elements

    Laravel provides both a simple mail message builder and the ability to create custom Blade views for more complex layouts.

  3. A trigger point in your code that:

    • Determines when to send the notification
    • Handles any conditions or checks
    • Initiates the sending process

    This could be an event listener, a controller action, or a scheduled command.

Setting Up Queued Notifications

For optimal performance, all our notification emails should be queued. This means they'll be processed in the background instead of making your users wait. Here's what you need:

  1. Configure your queue in .env:
    QUEUE_CONNECTION=redis    # or 'database' if you prefer

Why Redis? Redis is an in-memory data store that's perfect for queues because it's:

  • Extremely fast (operations happen in memory)
  • Reliable (persistence options available)
  • Easy to monitor and debug

If you don't have Redis available, you can use the database queue driver instead, which stores jobs in your application's database.

  1. Make sure a queue worker is running:
    php artisan queue:work

The queue worker is a process that continuously monitors your queue for new jobs. In production, you'll want to use a process manager like Supervisor to keep this running and restart it if it fails.

  1. Make your notification classes implement ShouldQueue:
    
    use Illuminate\Contracts\Queue\ShouldQueue;
    use Illuminate\Notifications\Notification;

class WelcomeEmail extends Notification implements ShouldQueue
{
// Your notification code here
}


By implementing <code>ShouldQueue, Laravel automatically handles queuing the notification for background processing.

## Example 1: Welcome Email After Verification

Let's create a welcome email that's sent after a user verifies their email address. This is a perfect first impression opportunity to guide new users through your application.

### Step 1: Create the Notification

First, generate the notification class:
```bash
php artisan make:notification WelcomeEmail

This creates a new file in app/Notifications. Let's break down the implementation:

<?php

namespace App\Notifications;

use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Contracts\Queue\ShouldQueue;

class WelcomeEmail extends Notification implements ShouldQueue
{
    public function via($notifiable)
    {
        return ['mail'];
    }

    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject('Welcome to ' . config('app.name') . '!')
            ->greeting("Hi {$notifiable->name}!")
            ->line('Thank you for verifying your email address. We're excited to have you on board!')
            ->line('Here are a few things you can do to get started:')
            ->line('• Create your first project')
            ->line('• Complete your profile')
            ->line('• Check out our getting started guide')
            ->action('Get Started', url('/dashboard'))
            ->line('If you have any questions, just reply to this email - we're always happy to help.');
    }
}

Let's break down what's happening here:

  • The via() method tells Laravel which notification channels to use. We're only using email here, but you could add additional channels like SMS or Slack.
  • The toMail() method builds our email using Laravel's fluent mail message builder:
    • subject() sets the email subject, using your app name from config
    • greeting() personalizes the email with the user's name
    • line() adds paragraphs of text
    • action() creates a button with a link
  • We use bullet points to make the "getting started" steps easy to scan
  • We end with a friendly invitation to get support

Step 2: Add the Trigger Point

We want to send this email right after the user verifies their email address. Laravel fires a Verified event when this happens, so we'll listen for it.

In your EventServiceProvider, add the listener:

<?php

namespace App\Providers;

use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        Verified::class => [
            'App\Listeners\SendWelcomeEmail',
        ],
    ];
}

Then create the listener:

php artisan make:listener SendWelcomeEmail
<?php

namespace App\Listeners;

use App\Notifications\WelcomeEmail;
use Illuminate\Auth\Events\Verified;

class SendWelcomeEmail
{
    public function handle(Verified $event)
    {
        $event->user->notify(new WelcomeEmail());
    }
}

The listener is simple but powerful:

  • It receives the Verified event, which contains the user who just verified their email
  • It uses Laravel's notify() method to send our welcome email
  • Because we implemented ShouldQueue, this happens in the background

Example 2: First Project Created Achievement

Celebrating user milestones is a great way to encourage engagement. Let's send an encouraging email when a user creates their first project.

Step 1: Create the Notification

php artisan make:notification FirstProjectCreated
<?php

namespace App\Notifications;

use App\Models\Project;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Contracts\Queue\ShouldQueue;

class FirstProjectCreated extends Notification implements ShouldQueue
{
    protected $project;

    public function __construct(Project $project)
    {
        $this->project = $project;
    }

    public function via($notifiable)
    {
        return ['mail'];
    }

    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject('Congratulations on Creating Your First Project! 🎉')
            ->greeting("Amazing work, {$notifiable->name}!")
            ->line("You've just created your first project: {$this->project->name}")
            ->line("This is a big step! Here are some things you might want to do next:")
            ->line('• Invite team members to collaborate')
            ->line('• Set up your first milestone')
            ->line('• Add some tasks to get started')
            ->action('View Your Project', url("/projects/{$this->project->id}"))
            ->line('Need help? Our support team is just a click away!');
    }
}

Key points about this notification:

  • We inject the Project model in the constructor so we can reference it in our email
  • We use an emoji in the subject line to make it more engaging
  • We provide clear next steps to guide the user
  • We include a direct link to their project
  • The tone is celebratory and encouraging

Step 2: Add the Trigger

In your ProjectController or service class:

<?php

namespace App\Http\Controllers;

use App\Models\Project;
use App\Notifications\FirstProjectCreated;

class ProjectController extends Controller
{
    public function store(Request $request)
    {
        $project = Project::create($request->validated());

        // If this is their first project, send the notification
        if ($request->user()->projects()->count() === 1) {
            $request->user()->notify(new FirstProjectCreated($project));
        }

        return redirect()->route('projects.show', $project);
    }
}

This implementation:

  • Creates the project first
  • Checks if this is the user's first project using a count query
  • Sends the notification only for the first project
  • Redirects to the new project page

The count query is efficient because:

  • It runs in the database rather than loading all projects
  • It stops counting after finding more than one project
  • It's atomic (no race conditions)

Example 3: Weekly Activity Digest

Weekly digests keep users engaged by showing them their progress and accomplishments. Let's create a comprehensive weekly summary email.

Step 1: Create the Notification

php artisan make:notification WeeklyDigest
<?php

namespace App\Notifications;

use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Contracts\Queue\ShouldQueue;

class WeeklyDigest extends Notification implements ShouldQueue
{
    protected $stats;

    public function __construct(array $stats)
    {
        $this->stats = $stats;
    }

    public function via($notifiable)
    {
        return ['mail'];
    }

    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject('Your Weekly Summary')
            ->markdown('emails.weekly-digest', [
                'user' => $notifiable,
                'stats' => $this->stats
            ]);
    }
}

Notice that instead of using the fluent interface, we're using a custom Markdown template. This gives us more flexibility in presenting complex data.

Step 2: Create a Custom Template

{{-- resources/views/emails/weekly-digest.blade.php --}}
@component('mail::message')
# Your Week in Review

Hi {{ $user->name }},

Here's what you accomplished this week:

@component('mail::panel')
## Activity Overview
- Projects Created: {{ $stats['projects_created'] }}
- Tasks Completed: {{ $stats['tasks_completed'] }}
- Team Members Added: {{ $stats['team_members_added'] }}
@endcomponent

@if($stats['projects_created'] > 0)
@component('mail::table')
| Project Name | Tasks | Status |
|:------------|:------|:--------|
@foreach($stats['recent_projects'] as $project)
| {{ $project->name }} | {{ $project->tasks_count }} | {{ $project->status }} |
@endforeach
@endcomponent
@endif

@component('mail::button', ['url' => url('/dashboard')])
View Dashboard
@endcomponent

Keep up the great work!<br>
{{ config('app.name') }}
@endcomponent

This template uses Laravel's built-in Markdown components to create a professional-looking email:

  • mail::message provides the base email layout
  • mail::panel creates a highlighted section for key stats
  • mail::table formats data in a clean, readable table
  • mail::button creates a styled call-to-action button
  • We use conditional rendering to only show the projects table if there are new projects

Step 3: Create an Artisan Command

php artisan make:command SendWeeklyDigests
<?php

namespace App\Console\Commands;

use App\Models\User;
use App\Notifications\WeeklyDigest;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;

class SendWeeklyDigests extends Command
{
    protected $signature = 'emails:weekly-digest';
    protected $description = 'Send weekly digest emails to all users';

    public function handle()
    {
        User::chunk(100, function ($users) {
            foreach ($users as $user) {
                $stats = $this->generateStats($user);

                // Only send if user had activity
                if ($this->hasActivity($stats)) {
                    $user->notify(new WeeklyDigest($stats));
                }
            }
        });
    }

    protected function generateStats($user)
    {
        $lastWeek = Carbon::now()->subWeek();

        return [
            'projects_created' => $user->projects()
                ->where('created_at', '>=', $lastWeek)
                ->count(),
            'tasks_completed' => $user->tasks()
                ->where('completed_at', '>=', $lastWeek)
                ->count(),
            'team_members_added' => $user->teamInvites()
                ->where('created_at', '>=', $lastWeek)
                ->count(),
            'recent_projects' => $user->projects()
                ->with('tasks')
                ->where('created_at', '>=', $lastWeek)
                ->get()
        ];
    }

    protected function hasActivity($stats)
    {
        return $stats['projects_created'] > 0 
            || $stats['tasks_completed'] > 0 
            || $stats['team_members_added'] > 0;
    }
}

This command is designed for efficiency:

  • Uses chunking to handle large numbers of users without memory issues
  • Only generates stats for the last week
  • Only sends emails to users who had activity
  • Uses eager loading (with('tasks')) to prevent N+1 queries

Step 4: Schedule the Command

In app/Console/Kernel.php:

protected function schedule(Schedule $schedule)
{
    $schedule->command('emails:weekly-digest')->weekly()->mondays()->at('9:00');
}

The scheduling is strategic:

  • Runs weekly on Monday mornings
  • 9:00 AM is typically when people start their work week
  • Using the scheduler means we don't need to maintain a separate cron entry

Example 4: Subscription Renewal Reminder

Subscription reminders are crucial for reducing churn and preventing surprise cancellations. Let's create a comprehensive reminder system.

Step 1: Create the Notification

php artisan make:notification SubscriptionRenewalReminder
<?php

namespace App\Notifications;

use Carbon\Carbon;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Contracts\Queue\ShouldQueue;

class SubscriptionRenewalReminder extends Notification implements ShouldQueue
{
    protected $daysLeft;
    protected $subscription;

    public function __construct($subscription)
    {
        $this->subscription = $subscription;
        $this->daysLeft = Carbon::now()->diffInDays($subscription->expires_at);
    }

    public function via($notifiable)
    {
        return ['mail'];
    }

    public function toMail($notifiable)
    {
        return (new MailMessage)
            ->subject('Your Subscription is Ending Soon')
            ->greeting("Hi {$notifiable->name}")
            ->line("Your {$this->subscription->plan->name} subscription will expire in {$this->daysLeft} days.")
            ->line("Don't worry - we'll automatically renew it using your saved payment method.")
            ->line('Here's what you'll be charged:')
            ->line("• {$this->subscription->plan->name}: ${$this->subscription->plan->price}")
            ->line('• Next billing date: ' . $this->subscription->expires_at->format('F j, Y'))
            ->action('Manage Subscription', url('/billing'))
            ->line('Need to update your billing information or change plans? Just click the button above.')
            ->line('Thank you for being a valued customer!');
    }
}

The subscription reminder notification:

  • Calculates days remaining automatically
  • References the specific subscription plan
  • Includes clear pricing information
  • Provides an easy way to update billing info
  • Uses a reassuring tone to prevent customer concern

Step 2: Create a Command to Send Reminders

php artisan make:command SendRenewalReminders
<?php

namespace App\Console\Commands;

use App\Models\Subscription;
use App\Notifications\SubscriptionRenewalReminder;
use Carbon\Carbon;
use Illuminate\Console\Command;

class SendRenewalReminders extends Command
{
    protected $signature = 'emails:renewal-reminders';
    protected $description = 'Send subscription renewal reminder emails';

    public function handle()
    {
        // Find subscriptions expiring in 7 days
        $expiringDate = Carbon::now()->addDays(7);

        Subscription::with(['user', 'plan'])
            ->whereBetween('expires_at', [
                $expiringDate->startOfDay(),
                $expiringDate->endOfDay()
            ])
            ->chunk(100, function ($subscriptions) {
                foreach ($subscriptions as $subscription) {
                    $subscription->user->notify(
                        new SubscriptionRenewalReminder($subscription)
                    );
                }
            });
    }
}

This command implements several important features:

  • Uses eager loading to efficiently load user and plan relationships
  • Uses whereBetween to precisely target subscriptions expiring on a specific day
  • Processes subscriptions in chunks to manage memory usage
  • Uses Carbon for reliable date handling

Step 3: Schedule the Command

protected function schedule(Schedule $schedule)
{
    $schedule->command('emails:renewal-reminders')->daily();
}

The daily scheduling ensures:

  • Each subscription gets exactly one reminder
  • The load is spread throughout the month
  • Users receive reminders at a consistent time
What I'm building

Delegate tasks. Get software.

Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.

Take a look at vroni.com

Subscribe to my newsletter

Get new posts when I publish them.

I respect your privacy. Unsubscribe at any time.

Leave a Reply

Your email address will not be published. Required fields are marked *