smajti1/laravel-wizard
A Laravel package for building multi-step form wizards with dedicated step classes, validation, navigation, and Blade views. It helps structure complex forms into manageable steps while keeping controller logic clean and configurable.
Install and publish the config/views:
composer require smajti1/laravel-wizard
php artisan vendor:publish --provider="Smajti1\Laravel\WizardServiceProvider"
Look first at config/wizard.php — it holds the key knobs you will actually touch: session_key, keep_alive, the classes map (slug → step class), view.layout / view.step / view.finished, and url.prefix / url.middleware. Then skim vendor/smajti1/laravel-wizard/src/Step.php and Wizard.php; that is the whole public API surface, and the README examples mirror it closely.
The canonical first use case is a multi-step form (registration, checkout, onboarding). The minimum viable shape is:
Smajti1\Laravel\Step — set $label, $view, and $rules; override process(Request $request) to capture input and save() to persist.Smajti1\Laravel\Wizard with a $steps array of slug → step class. The array order defines the flow.finished() action that renders view.finished and forgets session data.{step?} segment, wired to the prefix/middleware from the config.If you can render a step, validate it, and land on the finished view, the rest of the package is just variations on that skeleton.
One step, one class, one form. Keep each Step class thin: $rules for validation, process() for extracting request data into the step's $data, save() for the side effect. Resist putting business logic in the controller — the wizard calls save() for you at the right moment, and moving logic into the controller breaks the abstraction (and the back/forward semantics).
Push persistence into save(), not process(). process() runs on submission; save() runs when the step is considered complete. This matters because going back to a step re-renders it with its stored data and does not re-run save(), which is exactly the behaviour you want for "edit previous step without duplicating a record."
Use skip() for conditional branches. Returning true from a step's skip() removes it from the flow without removing it from $steps. Typical use: skip the shipping address when the user picked "pickup", skip a payment step for free plans. The wizard's getNextStep() / getPreviousStep() will jump past skipped steps, so your back-links stay correct for free.
Keep the controller a thin adapter. A single index() method that inspects the current step and returns the step view or redirects to the next slug; a finished() that renders the terminal view and calls forgetAllStepData(). Add the wizard's route group under your own auth middleware — the package's url.middleware is a sensible default but you usually want auth layered on top.
Session is the source of truth between steps. Read prior-step data via the wizard's getStepData() / getAllStepData() helpers when rendering the current step (e.g. showing a summary). Don't stash things in the controller or in hidden inputs — the session is what survives the redirect.
Customise views per step, not globally. Point a step's $view at a specific Blade file when it needs a different layout (review screen, upload step). Leave wizard.layout as the shared chrome so the progress indicator and nav buttons stay consistent.
save() runs on every forward transition — design for it. If the user goes forward, back, and forward again, save() fires each time. Make it an upsert (updateOrCreate, firstOrCreate) or gate it on $this->wasSaved if you genuinely need once-only behaviour. This is the most common source of duplicate rows in this package.
The session driver bites. Wizard payloads live in the session, so cookie-only sessions silently truncate or fail on anything past a few kilobytes. Use file, database, or redis. If a wizard "works locally, breaks in prod," check SESSION_DRIVER first.
keep_alive is not intuitive. Toggling it changes whether session data is cleared after the finished step. If you see stale wizard data appearing on a second run of the same user, you wanted it false (or you forgot to call forgetAllStepData() on finish).
Slug keys must match across three places. The $steps array keys, the classes map in config/wizard.php, and any {step} URL segment you build must agree. Renaming a key and forgetting the config is a five-minute debugging detour; php artisan config:clear afterwards, always.
Route/prefix drift breaks the back-button. The package generates URLs from url.prefix. If you change it in config but leave your hand-written route('wizard.index') links pointing elsewhere, next/previous links 404. Prefer route names over hardcoded paths in your step views.
Custom validation messages go on the step, not the controller. Override the messages array/method on Step rather than catching ValidationException in the controller — the package re-renders the step with $errors and old input only when validation is routed through it.
Debugging tip. dd(session(config('wizard.session_key'))) after each transition shows you exactly what the wizard thinks it has, which step is current, and which steps were skipped. Faster than stepping through the package source.
Version pinning is real here. 40 stars, last release Dec 2023, and no Laravel 11/12 guarantees in the metadata. Pin smajti1/laravel-wizard in composer.json, run your wizard smoke test on every Laravel minor upgrade, and treat the package as vendored — fork it if you need to patch the Step base class rather than waiting for a release.
Extension points worth knowing. Overriding Wizard::getNextStep() lets you implement branching beyond what skip() covers (e.g. jump from step 2 to step 5 based on a flag). The Step class is the right place to add cross-cutting helpers (file upload normalisation, address formatting) via a shared base class in your app — don't subclass Wizard for that.
How can I help you explore Laravel packages today?