ruwork/wizard-bundle
ruwork/wizard-bundle is a Symfony bundle for building multi-step wizard flows, helping organize forms and sequential user input into structured, manageable steps with navigation support.
Note upfront: this is a Symfony bundle, not a Laravel package. A Laravel developer will either (a) use it inside a Symfony sub-service/hybrid app, or (b) read it as a reference implementation for a wizard pattern and port ~200 lines into a Laravel service. Given it's archived with 1 star and no dependents, treat it as a design reference, not a dependency you install into a new Laravel app.
If you do install it (Symfony context):
composer require ruwork/wizard-bundle — expect to pin an old Symfony version; check composer.json constraints before committing.Ruwork\WizardBundle\RuworkWizardBundle in config/bundles.php.src/):
Wizard / WizardInterface — orchestrates the step sequence.StepInterface / AbstractStep — one form per step.SessionWizardStorage (or equivalent storage) — persists step state across requests.StepEvent, WizardEvent) — hook points for branching/validation.For Laravel, the minimal port: a Wizard service resolving the current step from a session key, each Step implementing FormRequest + handle(), plus a controller that advances/rewinds the pointer.
One step = one form type + one handler. The common shape is:
class AddressStep extends AbstractStep
{
public function getForm(array $data): FormInterface { /* build form */ }
public function handleForm(array $data): void { /* persist to storage */ }
public function isApplicable(array $data): bool { /* conditional skip */ }
}
Session-backed linear flow with explicit navigation. The controller pattern is almost always a redirect, not a render-through:
GET /wizard/step/{name} → render.POST /wizard/step/{name} → validate, handleForm, advance pointer, redirect() to the next step URL.Compose steps, don't inherit. Register each step as a tagged service; let the wizard receive an iterable of steps. This lets you reorder the flow in config or reorder via priority without editing classes.
Conditional branching via isApplicable(). Steps opt themselves out (e.g. "company details" only if user chose "business"). Keep the branch logic inside the step, not in the controller — otherwise the controller becomes a switch monster.
Event hooks for cross-cutting concerns. Use the step/wizard events to do things like: recompute totals, send partial-completion analytics, or invalidate downstream step data when an earlier step changes. In Laravel the equivalent is Event::dispatch(new StepCompleted(...)) inside the wizard service.
Storage abstraction. Keep WizardStorageInterface (or your own contract) in front of session data. Swap SessionWizardStorage for a database- or cache-backed implementation when a wizard must survive session expiry (e.g. long checkout abandonments).
Resume-on-return. On wizard entry, if storage has state, jump to the first incomplete step rather than step 1. This is the single biggest UX win and is nearly free once storage exists.
composer require this into a Laravel app and expect it to boot — bundles need Symfony's kernel. Port the pattern instead.FormInterface or entity manager object into session storage. Store plain arrays/IDs; rebuild forms on request.handleForm for all applicable steps at the end (or at least re-check isApplicable) — data may have been mutated or steps added since.isApplicable() must be cheap and side-effect-free. It's called on every step transition; doing DB writes there causes subtle bugs.src/DependencyInjection/Configuration.php for the exact keys — don't guess.isApplicable(), label() for a progress bar), and events (analytics, totals).Wizard service alone will miss session and form-boundary bugs.getSteps()/getProgress() on the wizard so your Blade/Twig template can render step indicators; don't hardcode step lists in the view.How can I help you explore Laravel packages today?