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.