Extending Laravel User Registration with Custom Event Listeners
Laravel's built-in user registration system is powerful, but often you need to perform additional actions when a user registers. By leveraging Laravel's event system, you can cleanly extend the registration process without modifying core authentication logic.
Understanding the Registration Event
When a user successfully registers, Laravel fires the Illuminate\Auth\Events\Registered
event. By default, this event triggers email verification, but you can add multiple custom listeners to handle additional tasks.
Setting Up Additional Event Listeners
Basic EventServiceProvider Configuration
// app/Providers/EventServiceProvider.php
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
Registered::class => [
// Default Laravel listener
SendEmailVerificationNotification::class,
// Your custom listeners
App\Listeners\CreateUserProfile::class,
App\Listeners\SendWelcomeEmail::class,
App\Listeners\NotifyAdminOfRegistration::class,
App\Listeners\SetupDefaultPreferences::class,
App\Listeners\TrackRegistrationSource::class,
App\Listeners\AssignDefaultRole::class,
],
];
}
Custom Listener Implementations
Creating User Profile
// app/Listeners/CreateUserProfile.php
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Registered;
use App\Models\UserProfile;
use Illuminate\Support\Str;
class CreateUserProfile
{
public function handle(Registered $event)
{
$user = $event->user;
UserProfile::create([
'user_id' => $user->id,
'display_name' => $user->name,
'username' => $this->generateUsername($user),
'bio' => null,
'avatar_url' => $this->generateDefaultAvatar($user),
'timezone' => $this->detectTimezone(),
'locale' => app()->getLocale(),
'theme' => 'light',
]);
}
private function generateUsername($user)
{
$baseUsername = Str::slug($user->name);
$username = $baseUsername;
$counter = 1;
while (UserProfile::where('username', $username)->exists()) {
$username = $baseUsername . $counter;
$counter++;
}
return $username;
}
private function generateDefaultAvatar($user)
{
$name = urlencode($user->name);
return "https://ui-avatars.com/api/?name={$name}&background=random&size=200";
}
private function detectTimezone()
{
// Try to detect timezone from request headers or IP
return request()->header('Timezone') ??
request()->header('X-Timezone') ??
'UTC';
}
}
Welcome Email System
// app/Listeners/SendWelcomeEmail.php
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Registered;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use App\Mail\WelcomeEmail;
use App\Models\EmailTemplate;
use Illuminate\Support\Facades\Mail;
class SendWelcomeEmail implements ShouldQueue
{
use InteractsWithQueue;
public $delay = 300; // Send after 5 minutes
public function handle(Registered $event)
{
$user = $event->user;
// Don't send if user is created by admin or import
if ($this->isAdminCreatedUser($user)) {
return;
}
// Get personalized welcome email template
$template = $this->getWelcomeTemplate($user);
Mail::to($user->email)->send(new WelcomeEmail($user, $template));
// Track email sent
$user->emailLogs()->create([
'type' => 'welcome',
'template_id' => $template->id ?? null,
'sent_at' => now(),
'status' => 'sent'
]);
}
private function isAdminCreatedUser($user)
{
return request()->is('admin/*') ||
request()->has('admin_created') ||
app()->runningInConsole();
}
private function getWelcomeTemplate($user)
{
// Return template based on user source or preferences
$source = session('registration_source', 'default');
return EmailTemplate::where('type', 'welcome')
->where('variant', $source)
->active()
->first() ??
EmailTemplate::where('type', 'welcome')
->where('variant', 'default')
->active()
->first();
}
}
User Preferences Setup
// app/Listeners/SetupDefaultPreferences.php
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Registered;
use App\Models\UserPreferences;
use App\Models\UserDashboardWidget;
class SetupDefaultPreferences
{
public function handle(Registered $event)
{
$user = $event->user;
// Create user preferences
UserPreferences::create([
'user_id' => $user->id,
'email_notifications' => true,
'push_notifications' => false,
'newsletter_subscribed' => $this->shouldSubscribeToNewsletter(),
'marketing_emails' => $this->shouldReceiveMarketing(),
'two_factor_enabled' => false,
'session_timeout' => 7200, // 2 hours
'data_export_format' => 'json',
]);
// Setup dashboard widgets
$this->setupDashboardWidgets($user);
// Set privacy preferences
$this->setupPrivacySettings($user);
// Configure notification preferences
$this->setupNotificationPreferences($user);
}
private function shouldSubscribeToNewsletter()
{
return request()->has('subscribe_newsletter') ||
session('newsletter_consent', false);
}
private function shouldReceiveMarketing()
{
return request()->has('marketing_consent') ||
session('marketing_consent', false);
}
private function setupDashboardWidgets($user)
{
$defaultWidgets = [
'welcome' => ['position' => 1, 'enabled' => true],
'recent_activity' => ['position' => 2, 'enabled' => true],
'quick_stats' => ['position' => 3, 'enabled' => false],
'notifications' => ['position' => 4, 'enabled' => true],
'calendar' => ['position' => 5, 'enabled' => false],
];
foreach ($defaultWidgets as $widget => $config) {
UserDashboardWidget::create([
'user_id' => $user->id,
'widget_type' => $widget,
'position' => $config['position'],
'enabled' => $config['enabled'],
'settings' => [],
]);
}
}
private function setupPrivacySettings($user)
{
$user->privacySettings()->create([
'profile_visibility' => 'public',
'show_email' => false,
'show_phone' => false,
'allow_indexing' => true,
'data_processing_consent' => now(),
'analytics_consent' => request()->has('analytics_consent'),
]);
}
private function setupNotificationPreferences($user)
{
$notificationTypes = [
'comment_replies' => true,
'mentions' => true,
'direct_messages' => true,
'system_updates' => true,
'promotional' => false,
'weekly_digest' => true,
];
foreach ($notificationTypes as $type => $enabled) {
$user->notificationPreferences()->create([
'type' => $type,
'email_enabled' => $enabled,
'push_enabled' => false,
'sms_enabled' => false,
]);
}
}
}
Admin Notification System
// app/Listeners/NotifyAdminOfRegistration.php
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Registered;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Models\User;
use App\Notifications\NewUserRegistration;
use Illuminate\Support\Facades\Notification;
class NotifyAdminOfRegistration implements ShouldQueue
{
public function handle(Registered $event)
{
$user = $event->user;
// Only notify for significant registrations
if ($this->shouldNotifyAdmin($user)) {
$this->sendAdminNotification($user);
}
// Track registration metrics
$this->trackRegistrationMetrics($user);
// Check for suspicious activity
$this->checkSuspiciousRegistration($user);
}
private function shouldNotifyAdmin($user)
{
// Notify if:
// 1. It's a business email domain
// 2. Registration from a new country
// 3. During business hours (optional)
$businessDomains = ['company.com', 'enterprise.org'];
$emailDomain = substr(strrchr($user->email, "@"), 1);
return in_array($emailDomain, $businessDomains) ||
$this->isNewCountryRegistration($user) ||
$this->isDuringBusinessHours();
}
private function sendAdminNotification($user)
{
$admins = User::role('admin')->get();
Notification::send($admins, new NewUserRegistration($user, [
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'referrer' => request()->header('referer'),
'registration_source' => session('utm_source'),
]));
}
private function trackRegistrationMetrics($user)
{
// Track registration analytics
app('analytics')->track('user_registered', [
'user_id' => $user->id,
'email_domain' => substr(strrchr($user->email, "@"), 1),
'registration_source' => session('utm_source'),
'referrer' => request()->header('referer'),
'ip_country' => $this->getCountryFromIP(),
'timestamp' => now()->toISOString(),
]);
}
private function checkSuspiciousRegistration($user)
{
$suspiciousIndicators = [];
// Check for disposable email
if ($this->isDisposableEmail($user->email)) {
$suspiciousIndicators[] = 'disposable_email';
}
// Check for rapid registrations from same IP
$recentRegistrations = User::where('created_at', '>=', now()->subHours(1))
->where('last_login_ip', request()->ip())
->count();
if ($recentRegistrations > 5) {
$suspiciousIndicators[] = 'rapid_registrations';
}
// Flag if suspicious
if (!empty($suspiciousIndicators)) {
$user->update(['is_flagged' => true]);
// Notify security team
Notification::route('slack', config('services.slack.security_webhook'))
->notify(new SuspiciousRegistration($user, $suspiciousIndicators));
}
}
}
Registration Analytics Tracking
// app/Listeners/TrackRegistrationSource.php
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Registered;
use App\Models\UserRegistrationSource;
use Illuminate\Support\Facades\DB;
class TrackRegistrationSource
{
public function handle(Registered $event)
{
$user = $event->user;
UserRegistrationSource::create([
'user_id' => $user->id,
'source' => $this->getRegistrationSource(),
'medium' => $this->getRegistrationMedium(),
'campaign' => $this->getRegistrationCampaign(),
'referrer' => request()->header('referer'),
'landing_page' => session('landing_page'),
'user_agent' => request()->userAgent(),
'ip_address' => request()->ip(),
'country' => $this->getCountryFromIP(),
'device_type' => $this->getDeviceType(),
'browser' => $this->getBrowser(),
]);
// Update campaign statistics
$this->updateCampaignStats();
}
private function getRegistrationSource()
{
return session('utm_source') ??
request()->get('source') ??
'direct';
}
private function getRegistrationMedium()
{
return session('utm_medium') ??
request()->get('medium') ??
'organic';
}
private function getRegistrationCampaign()
{
return session('utm_campaign') ??
request()->get('campaign') ??
null;
}
private function updateCampaignStats()
{
$source = $this->getRegistrationSource();
$campaign = $this->getRegistrationCampaign();
if ($campaign) {
DB::table('campaign_stats')
->where('campaign', $campaign)
->increment('registrations');
}
DB::table('source_stats')
->where('source', $source)
->increment('registrations');
}
}
Advanced Registration Workflows
Multi-Step Registration Process
// app/Listeners/InitiateOnboardingFlow.php
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Registered;
use App\Models\OnboardingStep;
use App\Jobs\SendOnboardingReminder;
class InitiateOnboardingFlow
{
public function handle(Registered $event)
{
$user = $event->user;
$onboardingSteps = [
'profile_completion' => [
'title' => 'Complete Your Profile',
'description' => 'Add your photo and bio',
'order' => 1,
'required' => true,
],
'email_verification' => [
'title' => 'Verify Your Email',
'description' => 'Check your inbox and click the verification link',
'order' => 2,
'required' => true,
],
'preferences_setup' => [
'title' => 'Set Your Preferences',
'description' => 'Customize your experience',
'order' => 3,
'required' => false,
],
'first_action' => [
'title' => 'Take Your First Action',
'description' => 'Create your first post or follow someone',
'order' => 4,
'required' => false,
],
];
foreach ($onboardingSteps as $step => $details) {
OnboardingStep::create([
'user_id' => $user->id,
'step' => $step,
'title' => $details['title'],
'description' => $details['description'],
'order' => $details['order'],
'required' => $details['required'],
'completed' => false,
]);
}
// Schedule onboarding reminders
SendOnboardingReminder::dispatch($user)->delay(now()->addHours(24));
SendOnboardingReminder::dispatch($user)->delay(now()->addDays(3));
SendOnboardingReminder::dispatch($user)->delay(now()->addWeek());
}
}
Role-Based Registration
// app/Listeners/AssignDefaultRole.php
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Registered;
use Spatie\Permission\Models\Role;
class AssignDefaultRole
{
public function handle(Registered $event)
{
$user = $event->user;
$role = $this->determineUserRole($user);
$user->assignRole($role);
// Set up role-specific defaults
$this->setupRoleDefaults($user, $role);
}
private function determineUserRole($user)
{
// Determine role based on email domain, registration source, etc.
$emailDomain = substr(strrchr($user->email, "@"), 1);
$enterpriseDomains = ['company.com', 'enterprise.org'];
if (in_array($emailDomain, $enterpriseDomains)) {
return 'business_user';
}
if (session('registration_type') === 'creator') {
return 'content_creator';
}
return 'regular_user';
}
private function setupRoleDefaults($user, $role)
{
switch ($role) {
case 'business_user':
$this->setupBusinessDefaults($user);
break;
case 'content_creator':
$this->setupCreatorDefaults($user);
break;
default:
$this->setupRegularDefaults($user);
}
}
private function setupBusinessDefaults($user)
{
$user->preferences()->update([
'dashboard_layout' => 'analytics',
'email_frequency' => 'daily',
'features' => ['advanced_analytics', 'team_collaboration'],
]);
}
private function setupCreatorDefaults($user)
{
$user->preferences()->update([
'dashboard_layout' => 'content',
'email_frequency' => 'weekly',
'features' => ['content_scheduling', 'audience_analytics'],
]);
}
}
Testing Registration Listeners
// tests/Feature/RegistrationListenersTest.php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Mail;
use App\Mail\WelcomeEmail;
class RegistrationListenersTest extends TestCase
{
public function test_user_profile_created_on_registration()
{
Event::fake();
$user = User::factory()->create();
Event::dispatch(new Registered($user));
$this->assertDatabaseHas('user_profiles', [
'user_id' => $user->id,
'display_name' => $user->name,
]);
$this->assertDatabaseHas('user_preferences', [
'user_id' => $user->id,
'email_notifications' => true,
]);
}
public function test_welcome_email_sent_on_registration()
{
Mail::fake();
$user = User::factory()->create();
Event::dispatch(new Registered($user));
Mail::assertQueued(WelcomeEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
}
public function test_admin_notified_for_business_registration()
{
Event::fake();
$admin = User::factory()->admin()->create();
$businessUser = User::factory()->create([
'email' => '[email protected]'
]);
Event::dispatch(new Registered($businessUser));
// Assert admin notification was sent
$this->assertTrue(true); // Add actual notification assertion
}
}
Best Practices
1. Keep Listeners Focused
Each listener should handle a single responsibility:
// ✅ Good: Single responsibility
class CreateUserProfile { /* Profile creation logic */ }
class SendWelcomeEmail { /* Email logic */ }
class TrackAnalytics { /* Analytics logic */ }
// ❌ Bad: Multiple responsibilities
class HandleRegistration {
// Profile creation, email sending, analytics, etc.
}
2. Make Listeners Queueable
For time-consuming tasks, implement ShouldQueue
:
class SendWelcomeEmail implements ShouldQueue
{
public $delay = 300; // 5 minute delay
public $tries = 3; // Retry failed jobs
}
3. Handle Failures Gracefully
class CreateUserProfile
{
public function handle(Registered $event)
{
try {
// Profile creation logic
} catch (Exception $e) {
Log::error('Failed to create user profile', [
'user_id' => $event->user->id,
'error' => $e->getMessage()
]);
// Don't throw - let other listeners continue
}
}
}
Conclusion
Extending Laravel's user registration with custom event listeners provides a clean, maintainable way to handle complex registration workflows. By leveraging the event system, you can:
- Maintain separation of concerns with focused listeners
- Scale functionality without modifying core auth logic
- Handle complex workflows like onboarding and role assignment
- Track analytics and metrics for business insights
- Ensure reliability with queued listeners and error handling
This approach makes your registration system flexible, testable, and easy to maintain as your application grows.