Long, daunting forms can severely impact user conversion rates. Breaking a massive data collection process into smaller, digestible chunks (a multi-step or "wizard" form) drastically improves user experience.
While Single Page Applications (SPAs) like Vue or React handle this naturally on the client side, building a solid multi-step form natively in server-side Laravel can be tricky. You need to persist state between page loads and validate data iteratively. In this tutorial, we will learn how to build one efficiently using Laravel sessions.
The Concept
We will build a simple 3-step checkout form:
- Step 1: Account Details (Name, Email)
- Step 2: Shipping Details (Address, City)
- Step 3: Review & Submit
We will store the validated data from each step into the Laravel session. Upon final submission, we will extract everything from the session, save it to the database, and clear the session.
Step 1: Routes
Define the routes for viewing and posting each step.
Route::get('checkout/step-1', [CheckoutController::class, 'step1'])->name('checkout.step1');
Route::post('checkout/step-1', [CheckoutController::class, 'postStep1']);
Route::get('checkout/step-2', [CheckoutController::class, 'step2'])->name('checkout.step2');
Route::post('checkout/step-2', [CheckoutController::class, 'postStep2']);
Route::get('checkout/step-3', [CheckoutController::class, 'step3'])->name('checkout.step3');
Route::post('checkout/submit', [CheckoutController::class, 'submit'])->name('checkout.submit');
Step 2: The Controller Logic
In our CheckoutController, we handle the validation and session storage.
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class CheckoutController extends Controller
{
public function step1(Request $request)
{
$data = $request->session()->get('checkout', []);
return view('checkout.step1', compact('data'));
}
public function postStep1(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255',
]);
$request->session()->put('checkout.step1', $validated);
return redirect()->route('checkout.step2');
}
public function step2(Request $request)
{
if (!$request->session()->has('checkout.step1')) {
return redirect()->route('checkout.step1');
}
$data = $request->session()->get('checkout', []);
return view('checkout.step2', compact('data'));
}
public function postStep2(Request $request)
{
$validated = $request->validate([
'address' => 'required|string',
'city' => 'required|string',
]);
$request->session()->put('checkout.step2', $validated);
return redirect()->route('checkout.step3');
}
}
Step 3: Review and Submission
The final step reads all the session data, displays it for review, and handles the database insertion upon confirmation.
public function step3(Request $request)
{
if (!$request->session()->has('checkout.step2')) {
return redirect()->route('checkout.step2');
}
$data = $request->session()->get('checkout');
return view('checkout.step3', compact('data'));
}
public function submit(Request $request)
{
$data = $request->session()->get('checkout');
// Insert into database...
// Order::create(array_merge($data['step1'], $data['step2']));
$request->session()->forget('checkout');
return redirect('/')->with('success', 'Order placed successfully!');
}
Step 4: The Views
Your blade files will be standard HTML forms. Be sure to populate the value attributes with session data so that if a user navigates back, they don't lose their progress.
<!-- resources/views/checkout/step1.blade.php -->
<form action="{{ route('checkout.step1') }}" method="POST">
@csrf
<input type="text" name="name" value="{{ old('name', data_get($data, 'step1.name')) }}" required>
<input type="email" name="email" value="{{ old('email', data_get($data, 'step1.email')) }}" required>
<button type="submit">Next Step</button>
</form>
Conclusion
By leveraging Laravel's powerful session management and built-in validation, you can easily break complex forms down into manageable chunks. This approach is highly scalable and ensures your application data remains secure and validated at every step of the process.