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 namespace App\Models;
Auto-pruning old records with Laravel's Prunable
pruning
eloquent
cleanup
Intermediate
5 steps
php
<?php namespace App\Http;
HTTP content negotiation in PHP
http
content-negotiation
parsing
Intermediate
9 steps
typescript
import { IsEmail, IsNotEmpty, IsOptional,
How validation groups reuse one DTO in NestJS
validation
dto
decorators
Intermediate
9 steps
php
<?php namespace App\Listeners;
Generating responsive images in a Laravel listener
queued jobs
event listeners
image processing
Intermediate
7 steps
typescript
import { InjectionToken, inject, Provider, isDevMode } from '@angular/core'; import { WINDOW } from './window.token'; export interface AnalyticsConfig {
Layered config with an Angular InjectionToken
dependency-injection
configuration
factory-provider
Intermediate
8 steps
php
final class InvoiceCalculator { private const SCALE = 4;
Precise money math with PHP's BCMath
bcmath
arbitrary precision
money
Intermediate
8 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.