php 145 lines · 4 tabs

Multi-Step Onboarding Wizard in Laravel with Per-Step Form Requests and Session Progress

Shared by codesnips Aug 2026
4 tabs
<?php

namespace App\Http\Requests\Wizard;

use Illuminate\Foundation\Http\FormRequest;

abstract class WizardStepRequest extends FormRequest
{
    abstract public function stepKey(): string;

    public function authorize(): bool
    {
        return true;
    }

    protected function passedValidation(): void
    {
        $steps = $this->session()->get('wizard.steps', []);
        $steps[$this->stepKey()] = $this->validated();

        $this->session()->put('wizard.steps', $steps);
    }

    public function previousData(string $step): array
    {
        return $this->session()->get("wizard.steps.{$step}", []);
    }
}
4 files · php Explain with highlit

This snippet builds a multi-step onboarding wizard where each step validates against its own dedicated form request and progress is accumulated in the session until a final commit. The central idea is to treat the wizard as a small state machine: an ordered list of step keys, a session bucket that holds validated data per step, and a rule that a user may only reach a step once every prior step has been completed. This keeps validation focused (each step only knows its own fields) while preserving partial input across requests without touching the database prematurely.

In WizardStepRequest, an abstract base request centralizes the shared plumbing. Concrete step requests declare their own rules(), but they all inherit stepKey() and a helper that persists validated data. The passedValidation() hook writes the just-validated payload into the session under wizard.steps.<key>, so by the time the controller runs, the data is already stored. This is the classic Laravel pattern of pushing side effects into the request lifecycle so controllers stay thin.

ProfileStepRequest and PreferencesStepRequest are the concrete steps. Each returns a distinct stepKey and a small, self-contained ruleset — note PreferencesStepRequest uses Rule::in and a nested array validation for topics, which is awkward to express if all steps shared one giant request class.

The EnsureStepIsReachable middleware enforces ordering. It reads the requested step from the route, walks the ordered WizardState::STEPS, and redirects back to the first incomplete step if the user jumped ahead. This prevents deep-linking into step three with an empty session, a common wizard bug.

OnboardingWizardController ties it together. show() renders the current step's view pre-filled from session; store() type-hints the correct request class per route so the framework resolves and runs the matching validator automatically. When the last step submits, complete() reads the aggregated session data, persists it in one transaction, and clears the wizard bucket via forget. The trade-off is that abandoned wizards leave stale session data until it expires, and session-bound state does not survive across devices — acceptable for short onboarding flows but not for long-running, resumable processes, which would call for a persisted draft record instead.


Related snips

ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
javascript
import { Controller } from "@hotwired/stimulus"
import Mousetrap from "mousetrap"

export default class extends Controller {
  connect() {
    // Global shortcuts

Keyboard shortcuts with Stimulus and Mousetrap

stimulus javascript ux
by Jordan Lee 2 tabs
javascript
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["form"]
  static values = { delay: { type: Number, default: 250 } }

Debounced live search with Stimulus + Turbo Streams

rails hotwire stimulus
by codesnips 4 tabs
php
<?php

namespace App\Providers;

use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;

Laravel service container and dependency injection

laravel dependency-injection service-container
by Carlos Mendez 2 tabs
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Validation Example</title>
  <style>

HTML forms with validation and accessibility

html forms validation
by Alex Chang 1 tab
javascript
import { Application } from "@hotwired/stimulus"
import FormSubmitController from "./controllers/form_submit_controller"

const application = Application.start()
application.debug = false

Disable submit button while Turbo form is submitting

rails hotwire stimulus
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Multi-Step Onboarding Wizard in Laravel with Per-Step Form Requests and Session Progress — share card
Link copied