Skip to content

Example User Registration

Jean-Marc Strauven edited this page Aug 6, 2025 · 2 revisions

πŸ‘€ Example: User Registration

← Artisan Commands | Home | Configuration β†’

This comprehensive example demonstrates a complete user registration workflow using Laravel Flowpipe, showcasing real-world patterns and best practices.

🎯 Overview

We'll build a user registration system that includes:

  • βœ… Input validation with custom rules
  • βœ… Email uniqueness checking
  • βœ… User account creation
  • βœ… Email verification system
  • βœ… Profile setup with defaults
  • βœ… Role assignment
  • βœ… Welcome communications
  • βœ… Analytics tracking
  • βœ… Error handling and recovery

πŸ“ Project Structure

flows/
└── user-registration.yaml

groups/
β”œβ”€β”€ user-validation.yaml
β”œβ”€β”€ user-setup.yaml
└── user-notifications.yaml

app/Flowpipe/Steps/UserRegistration/
β”œβ”€β”€ ValidateInputStep.php
β”œβ”€β”€ CheckEmailUniquenessStep.php
β”œβ”€β”€ CreateUserAccountStep.php
β”œβ”€β”€ SendVerificationEmailStep.php
β”œβ”€β”€ SetupUserProfileStep.php
β”œβ”€β”€ AssignDefaultRoleStep.php
β”œβ”€β”€ SendWelcomeEmailStep.php
└── LogRegistrationEventStep.php

app/Flowpipe/Conditions/
└── EmailVerificationEnabledCondition.php

πŸ“„ YAML Flow Definition

Main Flow: flows/user-registration.yaml

flow: user-registration
description: |
  Complete user registration workflow with validation, 
  email verification, and profile setup
version: "2.1"
tags: [user, registration, onboarding]

# Test data for development
send:
  name: "John Doe"
  email: "[email protected]"
  password: "SecurePassword123!"
  password_confirmation: "SecurePassword123!"
  terms_accepted: true
  newsletter_subscription: true
  referral_code: "FRIEND2024"

# Error handling configuration
timeout: 300000  # 5 minutes
on_error: compensate

compensation:
  - type: action
    name: cleanup-partial-registration
    class: App\Flowpipe\Steps\UserRegistration\CleanupPartialRegistrationStep
  - type: action
    name: notify-admin-registration-failure
    class: App\Flowpipe\Steps\UserRegistration\NotifyAdminFailureStep

steps:
  # Phase 1: Validation
  - type: group
    name: user-validation
    description: Comprehensive input validation
    
  # Phase 2: Account Creation
  - type: action
    name: create-user-account
    class: App\Flowpipe\Steps\UserRegistration\CreateUserAccountStep
    description: Create user account in database
    timeout: 30000
    
  # Phase 3: Setup and Configuration
  - type: group
    name: user-setup
    description: Configure user profile and preferences
    
  # Phase 4: Communications (conditional)
  - type: group
    name: user-notifications
    description: Send welcome communications
    when:
      field: send_notifications
      operator: not_equals
      value: false
      
  # Phase 5: Analytics and Tracking
  - type: action
    name: log-registration-event
    class: App\Flowpipe\Steps\UserRegistration\LogRegistrationEventStep
    description: Track registration for analytics
    on_error: continue  # Non-critical step

Validation Group: groups/user-validation.yaml

group: user-validation
description: Comprehensive user input validation with security checks
version: "1.3"

steps:
  # Basic input validation
  - type: action
    name: validate-input-format
    class: App\Flowpipe\Steps\UserRegistration\ValidateInputStep
    description: Validate email format, password strength, and required fields
    
  # Security checks
  - type: action
    name: check-email-uniqueness
    class: App\Flowpipe\Steps\UserRegistration\CheckEmailUniquenessStep
    description: Verify email address is not already registered
    
  # Optional: Advanced validation
  - type: action
    name: validate-referral-code
    class: App\Flowpipe\Steps\UserRegistration\ValidateReferralCodeStep
    description: Validate referral code if provided
    when:
      field: referral_code
      operator: exists
    on_error: continue  # Invalid referral doesn't stop registration

User Setup Group: groups/user-setup.yaml

group: user-setup
description: Configure user profile, preferences, and default settings
version: "1.1"

steps:
  # Profile setup
  - type: action
    name: setup-user-profile
    class: App\Flowpipe\Steps\UserRegistration\SetupUserProfileStep
    description: Initialize user profile with default settings
    
  # Role assignment
  - type: action
    name: assign-default-role
    class: App\Flowpipe\Steps\UserRegistration\AssignDefaultRoleStep
    description: Assign appropriate user role based on registration data
    
  # Handle referral rewards
  - type: action
    name: process-referral-reward
    class: App\Flowpipe\Steps\UserRegistration\ProcessReferralRewardStep
    description: Award referral rewards if applicable
    when:
      all:
        - field: referral_code
          operator: exists
        - field: referral_code_valid
          operator: equals
          value: true
    on_error: continue  # Don't fail registration if referral processing fails

Notifications Group: groups/user-notifications.yaml

group: user-notifications
description: Send welcome communications and verification emails
version: "1.2"

steps:
  # Email verification (conditional)
  - condition:
      custom: App\Flowpipe\Conditions\EmailVerificationEnabledCondition
    then:
      - type: action
        name: send-verification-email
        class: App\Flowpipe\Steps\UserRegistration\SendVerificationEmailStep
        description: Send email verification link
        config:
          template: "auth.verify-email"
          expires_in: 3600  # 1 hour
    else:
      - type: action
        name: mark-email-verified
        class: App\Flowpipe\Steps\UserRegistration\MarkEmailVerifiedStep
        
  # Welcome communications
  - type: action
    name: send-welcome-email
    class: App\Flowpipe\Steps\UserRegistration\SendWelcomeEmailStep
    description: Send personalized welcome email
    config:
      template: "auth.welcome"
      include_getting_started: true
    on_error: continue  # Don't fail registration if welcome email fails
    
  # Newsletter subscription (conditional)
  - type: action
    name: subscribe-to-newsletter
    class: App\Flowpipe\Steps\UserRegistration\SubscribeToNewsletterStep
    description: Subscribe user to newsletter if opted in
    when:
      field: newsletter_subscription
      operator: equals
      value: true
    on_error: continue  # Non-critical

πŸ—οΈ PHP Step Implementations

Input Validation Step

<?php

namespace App\Flowpipe\Steps\UserRegistration;

use Closure;
use Grazulex\LaravelFlowpipe\Contracts\FlowStep;
use Illuminate\Support\Facades\Validator;
use InvalidArgumentException;

class ValidateInputStep implements FlowStep
{
    public function handle(mixed $payload, Closure $next): mixed
    {
        $validator = Validator::make($payload, [
            'name' => [
                'required', 
                'string', 
                'max:255',
                'regex:/^[a-zA-Z\s\-\'\.]+$/'  // Only letters, spaces, hyphens, apostrophes, dots
            ],
            'email' => [
                'required', 
                'email:rfc,dns', 
                'max:255',
                'not_regex:/\+.*@/'  // Prevent plus addressing for simpler management
            ],
            'password' => [
                'required',
                'min:8',
                'max:128',
                'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/',
                'confirmed'
            ],
            'terms_accepted' => 'required|boolean|accepted',
            'newsletter_subscription' => 'boolean',
            'referral_code' => 'nullable|string|max:50|alpha_num',
        ], [
            'name.regex' => 'Name can only contain letters, spaces, hyphens, apostrophes, and periods.',
            'email.not_regex' => 'Plus addressing is not supported.',
            'password.regex' => 'Password must contain at least one uppercase letter, lowercase letter, number, and special character.',
        ]);

        if ($validator->fails()) {
            throw new InvalidArgumentException(
                'Validation failed: ' . implode(', ', $validator->errors()->all())
            );
        }

        // Add validation timestamp for tracking
        $validated = $validator->validated();
        $validated['validated_at'] = now();
        $validated['validation_ip'] = request()->ip();

        return $next($validated);
    }
}

Email Uniqueness Check Step

<?php

namespace App\Flowpipe\Steps\UserRegistration;

use Closure;
use Grazulex\LaravelFlowpipe\Contracts\FlowStep;
use App\Models\User;
use InvalidArgumentException;
use Illuminate\Support\Facades\Log;

class CheckEmailUniquenessStep implements FlowStep
{
    public function handle(mixed $payload, Closure $next): mixed
    {
        $email = strtolower(trim($payload['email']));
        
        // Check active users
        $existingUser = User::where('email', $email)
            ->whereNull('deleted_at')
            ->first();
            
        if ($existingUser) {
            Log::warning('Registration attempted with existing email', [
                'email' => $email,
                'existing_user_id' => $existingUser->id,
                'ip' => request()->ip()
            ]);
            
            throw new InvalidArgumentException(
                'An account with this email address already exists.'
            );
        }
        
        // Check recently deleted users (within 30 days)
        $recentlyDeleted = User::onlyTrashed()
            ->where('email', $email)
            ->where('deleted_at', '>', now()->subDays(30))
            ->first();
            
        if ($recentlyDeleted) {
            throw new InvalidArgumentException(
                'This email was recently used for an account. Please contact support to reactivate or use a different email.'
            );
        }
        
        // Normalize email for consistency
        $payload['email'] = $email;
        $payload['email_unique_verified'] = true;
        $payload['uniqueness_checked_at'] = now();
        
        return $next($payload);
    }
}

User Account Creation Step

<?php

namespace App\Flowpipe\Steps\UserRegistration;

use Closure;
use Grazulex\LaravelFlowpipe\Contracts\FlowStep;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class CreateUserAccountStep implements FlowStep
{
    public function handle(mixed $payload, Closure $next): mixed
    {
        DB::beginTransaction();
        
        try {
            $user = User::create([
                'name' => $payload['name'],
                'email' => $payload['email'],
                'password' => Hash::make($payload['password']),
                'email_verification_token' => Str::random(60),
                'email_verified_at' => null, // Will be set after verification
                'registration_ip' => $payload['validation_ip'] ?? request()->ip(),
                'registration_user_agent' => request()->userAgent(),
                'newsletter_subscribed' => $payload['newsletter_subscription'] ?? false,
                'referral_code_used' => $payload['referral_code'] ?? null,
            ]);
            
            // Generate API token for mobile apps
            $token = $user->createToken('registration-token')->plainTextToken;
            
            DB::commit();
            
            // Add user data to payload for subsequent steps
            $payload['user'] = $user;
            $payload['user_id'] = $user->id;
            $payload['api_token'] = $token;
            $payload['email_verification_token'] = $user->email_verification_token;
            
            // Remove sensitive data
            unset($payload['password'], $payload['password_confirmation']);
            
            return $next($payload);
            
        } catch (\Exception $e) {
            DB::rollBack();
            throw $e;
        }
    }
}

Profile Setup Step

<?php

namespace App\Flowpipe\Steps\UserRegistration;

use Closure;
use Grazulex\LaravelFlowpipe\Contracts\FlowStep;
use App\Models\UserProfile;
use Illuminate\Support\Facades\Http;

class SetupUserProfileStep implements FlowStep
{
    public function handle(mixed $payload, Closure $next): mixed
    {
        $user = $payload['user'];
        
        // Create user profile with defaults
        $profile = UserProfile::create([
            'user_id' => $user->id,
            'timezone' => $this->detectTimezone($payload),
            'language' => 'en',
            'theme' => 'light',
            'email_notifications' => true,
            'push_notifications' => true,
            'marketing_emails' => $payload['newsletter_subscription'] ?? false,
            'profile_visibility' => 'private',
            'two_factor_enabled' => false,
        ]);
        
        // Set user preferences
        $user->preferences()->createMany([
            ['key' => 'welcome_tour_completed', 'value' => false],
            ['key' => 'onboarding_step', 'value' => 'profile_setup'],
            ['key' => 'registration_source', 'value' => $this->detectRegistrationSource($payload)],
        ]);
        
        $payload['user_profile'] = $profile;
        $payload['profile_setup_completed'] = true;
        
        return $next($payload);
    }
    
    private function detectTimezone(array $payload): string
    {
        // Try to detect timezone from IP address
        try {
            $ip = $payload['validation_ip'] ?? request()->ip();
            $response = Http::timeout(5)->get("http://ip-api.com/json/{$ip}");
            
            if ($response->successful() && $response->json('timezone')) {
                return $response->json('timezone');
            }
        } catch (\Exception $e) {
            // Ignore timezone detection failures
        }
        
        return 'UTC'; // Default fallback
    }
    
    private function detectRegistrationSource(array $payload): string
    {
        if (isset($payload['referral_code'])) {
            return 'referral';
        }
        
        $userAgent = request()->userAgent();
        if (str_contains($userAgent, 'Mobile')) {
            return 'mobile';
        }
        
        return 'web';
    }
}

Email Verification Step

<?php

namespace App\Flowpipe\Steps\UserRegistration;

use Closure;
use Grazulex\LaravelFlowpipe\Contracts\FlowStep;
use Illuminate\Support\Facades\Mail;
use App\Mail\EmailVerificationMail;

class SendVerificationEmailStep implements FlowStep
{
    public function __construct(private array $config = [])
    {
    }
    
    public function handle(mixed $payload, Closure $next): mixed
    {
        $user = $payload['user'];
        $template = $this->config['template'] ?? 'auth.verify-email';
        $expiresIn = $this->config['expires_in'] ?? 3600;
        
        // Generate verification URL
        $verificationUrl = route('verification.verify', [
            'id' => $user->id,
            'hash' => sha1($user->email),
            'token' => $payload['email_verification_token'],
            'expires' => now()->addSeconds($expiresIn)->timestamp,
        ]);
        
        // Send verification email
        Mail::to($user)->send(new EmailVerificationMail(
            $user,
            $verificationUrl,
            $expiresIn
        ));
        
        $payload['verification_email_sent'] = true;
        $payload['verification_email_sent_at'] = now();
        $payload['verification_expires_at'] = now()->addSeconds($expiresIn);
        
        return $next($payload);
    }
}

πŸ”§ Custom Condition

Email Verification Condition

<?php

namespace App\Flowpipe\Conditions;

use Grazulex\LaravelFlowpipe\Contracts\Condition;

class EmailVerificationEnabledCondition implements Condition
{
    public function evaluate(array $payload, array $context = []): bool
    {
        // Check global setting
        if (!config('auth.email_verification_enabled', true)) {
            return false;
        }
        
        // Check if user explicitly requested to skip verification (admin creation)
        if (isset($payload['skip_email_verification']) && $payload['skip_email_verification']) {
            return false;
        }
        
        // Check if this is a trusted domain (internal employees)
        $email = $payload['email'] ?? '';
        $trustedDomains = config('auth.trusted_domains', []);
        
        foreach ($trustedDomains as $domain) {
            if (str_ends_with($email, "@{$domain}")) {
                return false; // Skip verification for trusted domains
            }
        }
        
        return true;
    }
}

πŸ§ͺ Testing the Flow

Feature Test

<?php

namespace Tests\Feature\Flowpipe;

use Tests\TestCase;
use Grazulex\LaravelFlowpipe\Flowpipe;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;

class UserRegistrationFlowTest extends TestCase
{
    use RefreshDatabase;

    public function test_complete_user_registration_flow()
    {
        Mail::fake();
        
        $userData = [
            'name' => 'John Doe',
            'email' => '[email protected]',
            'password' => 'SecurePassword123!',
            'password_confirmation' => 'SecurePassword123!',
            'terms_accepted' => true,
            'newsletter_subscription' => true,
        ];

        $result = Flowpipe::fromYaml('user-registration')
            ->send($userData)
            ->execute();

        // Assert flow completed successfully
        $this->assertTrue($result->isSuccessful());
        
        // Assert user was created
        $this->assertDatabaseHas('users', [
            'name' => 'John Doe',
            'email' => '[email protected]',
        ]);
        
        // Assert profile was created
        $user = User::where('email', '[email protected]')->first();
        $this->assertNotNull($user->profile);
        
        // Assert emails were sent
        Mail::assertSent(EmailVerificationMail::class);
        
        // Assert result contains expected data
        $this->assertEquals($user->id, $result->data['user_id']);
        $this->assertTrue($result->data['verification_email_sent']);
        $this->assertTrue($result->data['profile_setup_completed']);
    }
    
    public function test_registration_fails_with_existing_email()
    {
        // Create existing user
        User::factory()->create(['email' => '[email protected]']);
        
        $userData = [
            'name' => 'John Doe',
            'email' => '[email protected]',
            'password' => 'SecurePassword123!',
            'password_confirmation' => 'SecurePassword123!',
            'terms_accepted' => true,
        ];

        $result = Flowpipe::fromYaml('user-registration')
            ->send($userData)
            ->execute();

        $this->assertFalse($result->isSuccessful());
        $this->assertStringContains('already exists', $result->error->getMessage());
    }
}

πŸš€ Usage Examples

Basic Registration

use Grazulex\LaravelFlowpipe\Flowpipe;

$result = Flowpipe::fromYaml('user-registration')
    ->send([
        'name' => 'Jane Smith',
        'email' => '[email protected]',
        'password' => 'SecurePassword456!',
        'password_confirmation' => 'SecurePassword456!',
        'terms_accepted' => true,
    ])
    ->execute();

if ($result->isSuccessful()) {
    return response()->json([
        'success' => true,
        'user_id' => $result->data['user_id'],
        'verification_required' => $result->data['verification_email_sent'],
    ]);
}

API Registration with Queue

// In a controller
public function register(Request $request)
{
    // Queue the registration for background processing
    $jobId = Flowpipe::fromYaml('user-registration')
        ->send($request->all())
        ->dispatch(); // Returns job ID
        
    return response()->json([
        'success' => true,
        'job_id' => $jobId,
        'message' => 'Registration is being processed',
    ], 202);
}

Admin Registration (Skip Verification)

$result = Flowpipe::fromYaml('user-registration')
    ->send([
        'name' => 'Admin User',
        'email' => '[email protected]',
        'password' => 'AdminPassword123!',
        'password_confirmation' => 'AdminPassword123!',
        'terms_accepted' => true,
        'skip_email_verification' => true,
        'send_notifications' => false,
    ])
    ->execute();

πŸ’‘ Best Practices Demonstrated

  1. Modular Design: Using groups to organize related steps
  2. Error Handling: Compensation strategy and graceful degradation
  3. Security: Input validation, password requirements, email verification
  4. Flexibility: Conditional logic for different scenarios
  5. Observability: Comprehensive logging and tracking
  6. Testing: Complete test coverage
  7. Documentation: Clear, self-documenting YAML

🎯 What's Next?

πŸš€ Laravel Flowpipe

🏠 Home

🏁 Getting Started

πŸ“š Core Concepts

πŸš€ Advanced Features

πŸ› οΈ Tools & Configuration

πŸ“– Examples


πŸ”— GitHub Repository

Clone this wiki locally