php
69 lines · 9 steps
Building a multi-step onboarding wizard in Laravel
A Laravel controller drives a session-backed wizard, validating each step and assembling the pieces into one final record.
Explained by
highlit
1<?php
2
3namespace App\Http\Controllers;
4
5use Illuminate\Http\Request;
6use Illuminate\Support\Facades\Validator;
7use App\Models\Registration;
8
9class OnboardingWizardController extends Controller
10{
11 private const SESSION_KEY = 'onboarding.wizard';
12
13 private array $steps = [
14 'account' => ['name' => 'required|string|max:120', 'email' => 'required|email'],
15 'company' => ['company' => 'required|string|max:200', 'size' => 'required|in:solo,small,large'],
16 'billing' => ['plan' => 'required|in:free,pro,enterprise'],
17 ];
18
19 public function show(Request $request, string $step)
20 {
21 abort_unless(array_key_exists($step, $this->steps), 404);
22
23 $state = $request->session()->get(self::SESSION_KEY, []);
24
25 return view("onboarding.{$step}", [
26 'step' => $step,
27 'data' => $state[$step] ?? [],
28 'progress' => $this->progress($step),
29 ]);
30 }
31
32 public function store(Request $request, string $step)
33 {
34 abort_unless(array_key_exists($step, $this->steps), 404);
35
36 $validated = Validator::make($request->all(), $this->steps[$step])->validate();
37
38 $request->session()->put(self::SESSION_KEY . ".{$step}", $validated);
39
40 $keys = array_keys($this->steps);
41 $next = $keys[array_search($step, $keys) + 1] ?? null;
42
43 if ($next === null) {
44 return $this->finish($request);
45 }
46
47 return redirect()->route('onboarding.show', ['step' => $next]);
48 }
49
50 private function finish(Request $request)
51 {
52 $state = $request->session()->get(self::SESSION_KEY, []);
53
54 $registration = Registration::create(array_merge(...array_values($state)));
55
56 $request->session()->forget(self::SESSION_KEY);
57
58 return redirect()
59 ->route('dashboard')
60 ->with('status', "Welcome aboard, {$registration->name}!");
61 }
62
63 private function progress(string $current): int
64 {
65 $keys = array_keys($this->steps);
66
67 return (int) round((array_search($current, $keys) + 1) / count($keys) * 100);
68 }
69}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Storing partial form state in the session lets a wizard survive across separate HTTP requests.
- 2Defining each step's rules in one data structure keeps validation, ordering, and progress in sync.
- 3Merging the accumulated per-step state into a single create call defers persistence until the flow completes.
Related explainers
php
<?php class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
rust
use chrono::{Duration, NaiveDate}; #[derive(Debug)] pub struct DateRange {
Parsing and iterating date ranges in Rust
error-handling
iterators
parsing
Intermediate
7 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-multi-step-onboarding-wizard-in-laravel-explained-php-30d3/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.