lexal/stepped-form
A Laravel package for building stepped, multi-step forms with ease, letting you split long forms into manageable wizard-style pages while handling validation and state across each step.
composer require lexal/stepped-form
The package is small (single-digit stars, zero dependents), so treat the source as the documentation. Spend your first ten minutes in this order:
README.md — the canonical usage snippet.src/ — find the base class you extend (typically something like SteppedForm) and the trait/contract that defines step lifecycle (steps(), render(), handle()).resources/views/ — if the package ships Blade stubs, publish them (php artisan vendor:publish --provider="Lexal\SteppedForm\..." or check for a tagged publish group).composer.json — confirm the Laravel/illuminate/* constraint matches your app version; with low adoption you won't get a compatibility patch quickly.First use case — a 3-step onboarding wizard:
// app/Forms/OnboardingForm.php
namespace App\Forms;
use Lexal\SteppedForm\...\SteppedForm;
class OnboardingForm extends SteppedForm
{
public function steps(): array
{
return [
'account' => ['view' => 'forms.steps.account', 'rules' => ['email' => 'required|email']],
'profile' => ['view' => 'forms.steps.profile', 'rules' => ['name' => 'required|string']],
'confirm' => ['view' => 'forms.steps.confirm', 'rules' => []],
];
}
}
// routes/web.php
Route::get('/onboarding/{step?}', [OnboardingController::class, 'show']);
Route::post('/onboarding/{step}', [OnboardingController::class, 'store']);
public function show(OnboardingForm $form, ?string $step = null)
{
return view('onboarding', ['step' => $form->render($step)]);
}
Render the step's view inside a shell layout that renders the stepper/progress indicator once, so individual step partials stay dumb.
Controller-as-orchestrator. Keep the form object stateless-ish and let the controller own the request cycle. render($step) returns a view (or view data); handle($request) validates the current step and returns the next step key or a redirect. Your controller becomes a thin switch on that return value.
Session-backed progress. Stepped forms fundamentally need state between requests. Expect the package to stash collected data under a session key (inspect src/ for the exact key format). Pattern:
public function store(Request $request, OnboardingForm $form, string $step)
{
$result = $form->handle($step, $request->all());
if ($result->hasErrors()) {
return back()->withErrors($result->errors())->withInput();
}
if ($result->isComplete()) {
$data = $result->data();
Onboarding::create($data); // persist
$form->forget(); // clear session state
return redirect()->route('dashboard');
}
return redirect()->route('onboarding', $result->nextStep());
}
Per-step validation, not whole-form validation. The value of a stepped form is validating one slice at a time. Define rules() keyed by step and make sure your handle() call only pulls the current step's rules — never $request->validate($allRules).
Back navigation. Implement "previous" as a separate route or a hidden _direction=back input that rewinds the pointer without validating or re-saving. If the package's handle() always validates, add an explicit previous()/step(-1) call rather than posting an empty payload.
Branching steps. For conditional flows (e.g. skip "company details" for individuals) override the step-resolution method — usually nextStep() — and return a different key based on already-collected data. Keep branching logic in one place; scattering it across controllers is the fastest way to create unreachable steps.
Review step. Treat the summary as just another step whose view renders $form->data() read-only. This is where session state earns its keep and where you'll catch half the integration bugs.
Testing. Feature-test the wizard as a sequence of POSTs, asserting session state and the final DB write:
$this->post('/onboarding/account', ['email' => '[email protected]'])->assertRedirect('/onboarding/profile');
$this->post('/onboarding/profile', ['name' => 'Ada'])->assertRedirect('/onboarding/confirm');
Livewire pairing. If you're on Livewire, don't mix paradigms — either wrap the whole wizard in one Livewire component and call render()/handle() from actions, or stay fully controller-driven. Session state and Livewire's component state will fight if you split them.
composer.json ("lexal/stepped-form": "1.2.*" rather than ^1.2) and diff the source on upgrade. The last release date is recent, which suggests active churn — read the changelog before bumping.old() for anything except the immediately preceding step's validation failure.resources/views. Publish them, then override; check view() calls in src/ for the exact namespace.steps(), rules(), nextStep(), and the step view path. Avoid touching vendor code — the fork will rot immediately given the release cadence.dd(session()->all()) after each step to see exactly what the package is persisting; the key naming will tell you whether state is namespaced. Add a temporary Log::info() in the base class's handle() if step transitions misbehave.session() array and per-step rule sets in an afternoon. Budget for that rather than fighting the API.How can I help you explore Laravel packages today?