-
-
Notifications
You must be signed in to change notification settings - Fork 0
Example User Registration
Jean-Marc Strauven edited this page Aug 6, 2025
·
2 revisions
β Artisan Commands | Home | Configuration β
This comprehensive example demonstrates a complete user registration workflow using Laravel Flowpipe, showcasing real-world patterns and best practices.
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
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
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
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
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
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
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);
}
}
<?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);
}
}
<?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;
}
}
}
<?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';
}
}
<?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);
}
}
<?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;
}
}
<?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());
}
}
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'],
]);
}
// 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);
}
$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();
- Modular Design: Using groups to organize related steps
- Error Handling: Compensation strategy and graceful degradation
- Security: Input validation, password requirements, email verification
- Flexibility: Conditional logic for different scenarios
- Observability: Comprehensive logging and tracking
- Testing: Complete test coverage
- Documentation: Clear, self-documenting YAML
- Error Handling - Robust error handling patterns
- Queue Integration - Asynchronous processing
- Step Groups - Organize workflow components
- Conditions & Branching - Dynamic workflow logic
Laravel Flowpipe - YAML-driven workflow engine for Laravel
GitHub: Laravel Flowpipe Repository | Support: GitHub Issues
Quick Navigation: Home β’ Installation β’ Configuration β’ Commands β’ Examples
π§ Developed by Grazulex