Speeding Up Laravel Tests with tmpfs (MySQL in RAM)
If your Laravel test suite is painfully slow and does a lot of database work (multi-tenancy, migrations, seeding), the bottleneck is almost…
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:
Before we dive into specific examples, let's understand how Laravel's notification system works and how we can use it to send emails.
A Laravel notification consists of three main components:
A notification class that defines:
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.
An email template that controls:
Laravel provides both a simple mail message builder and the ability to create custom Blade views for more complex layouts.
A trigger point in your code that:
This could be an event listener, a controller action, or a scheduled command.
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:
.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:
If you don't have Redis available, you can use the database queue driver instead, which stores jobs in your application's database.
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.
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:
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.toMail() method builds our email using Laravel's fluent mail message builder:
subject() sets the email subject, using your app name from configgreeting() personalizes the email with the user's nameline() adds paragraphs of textaction() creates a button with a linkWe 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:
Verified event, which contains the user who just verified their emailnotify() method to send our welcome emailShouldQueue, this happens in the backgroundCelebrating user milestones is a great way to encourage engagement. Let's send an encouraging email when a user creates their first project.
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:
Project model in the constructor so we can reference it in our emailIn 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:
The count query is efficient because:
Weekly digests keep users engaged by showing them their progress and accomplishments. Let's create a comprehensive weekly summary email.
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.
{{-- 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 layoutmail::panel creates a highlighted section for key statsmail::table formats data in a clean, readable tablemail::button creates a styled call-to-action buttonphp 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:
with('tasks')) to prevent N+1 queriesIn app/Console/Kernel.php:
protected function schedule(Schedule $schedule)
{
$schedule->command('emails:weekly-digest')->weekly()->mondays()->at('9:00');
}
The scheduling is strategic:
Subscription reminders are crucial for reducing churn and preventing surprise cancellations. Let's create a comprehensive reminder system.
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:
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:
whereBetween to precisely target subscriptions expiring on a specific dayprotected function schedule(Schedule $schedule)
{
$schedule->command('emails:renewal-reminders')->daily();
}
The daily scheduling ensures:
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