ruwork/wizard
Ruwork Wizard is a lightweight PHP package for building step-by-step forms and guided workflows. Define ordered steps, manage navigation and validation, and turn complex processes into clear, sequential wizards with a simple, reusable API. Ideal for onboarding, checkout, and other multi-stage user j...
Before writing a single line against ruwork/wizard, confirm what you actually have. The repo is archived, has one star, and zero dependents — treat it as sample code, not a maintained dependency.
composer require it. Even if a Packagist entry exists, an archived package can disappear or break on a PHP minor. Add it as a VCS repository with an explicit commit hash, or copy the relevant src/ classes into app/Support/Wizard/ so you own the code.tests/ before README.md. With a 1-star RuVents package the tests are usually the only accurate documentation of intended usage — the README is often a stub. Look for the fixture that wires a multi-step flow end-to-end; that is your mental model.*Bundle, DependencyInjection/, Resources/config/services.yaml). If you see those directories, there is no Laravel service provider and you will be hand-wiring container bindings yourself.First use case: a multi-step onboarding or checkout form where each step validates independently, the user can move back and forward without losing data, and only the final step commits to the database. Build this thin vertical slice first — step 1 → step 2 → submit — before touching branching or persistence.
// Realistic Laravel adaptation: wrap the package's flow in a service you control
class CheckoutWizard
{
public function __construct(private Wizard $wizard) {}
public function advance(Request $request): WizardState
{
$step = $this->wizard->currentStep();
$data = $request->validate($step->rules());
return $this->wizard->withData($step->name(), $data)->next();
}
}
Per-step validation with a shared data bag. The pattern that makes wizards pleasant is: each step validates its own slice, results merge into one accumulated array, and the final step validates the whole thing. Implement step rules as plain arrays and validate with Laravel's validator directly — do not let the package own validation, because Laravel's FormRequest and error-bag semantics are what your Blade views already expect.
foreach ($step->rules() as $field => $rules) { /* prefix errors with step name */ }
Session-backed state with a stable key. Persist wizard state in the session under a key scoped to the domain object (checkout.42.wizard), not a bare wizard key. Two tabs, or a login mid-flow, will otherwise clobber each other. If the package ships its own storage interface, implement it over Session rather than using its filesystem/array default.
Redirect-after-POST between steps. Never render the next step as the response to a POST. Redirect to GET /checkout/step/{n}, which rehydrates from session. This kills double-submit bugs and makes the browser back button behave.
return redirect()->route('checkout.step', $this->wizard->next()->name());
Guard the step route. A GET /checkout/step/3 must 404 or redirect if the wizard has not completed step 2. Expose $wizard->canAccess($step) and enforce it in middleware — this is the single most commonly forgotten piece in wizard implementations.
Branching via a nextStep() hook. If the package supports conditional steps, keep the branch logic in your own step class (ShippingStep::nextStep() returns BillingStep or PickupStep) rather than in controllers. If the package does not support it, don't fight it — resolve the next step name yourself and call goto($name).
Commit on finalize, inside a transaction. The last step should call a single finalize() that maps accumulated state to Eloquent models in one DB::transaction(), then clears the wizard session key. Keep the mapping in a dedicated CheckoutFinalizer so it is testable without HTTP.
View integration. Expose $wizard->steps(), $wizard->current(), $wizard->progress() to a single checkout.layout Blade with a stepper partial. Avoid one view per step — a @include("checkout.steps.{$step->name()}") keeps navigation chrome consistent.
composer audit yourself, and budget a day to replace it if it becomes load-bearing. For a wizard, native Laravel (session array + a Step interface + route model binding) is roughly 200 lines and is a genuinely competitive option.services.yaml, none of it is autowired in Laravel. Trace every new and $this->get(...) in src/ and bind them explicitly in a WizardServiceProvider.currentIndex / total is wrong at both ends. Decide whether progress means "steps completed" or "steps started" and assert it in a test, because the package's own progress() will be untested and untrustworthy.name field will merge error messages into the same bag. Prefix keys (shipping.name) or use a named error bag per step.WizardState::toArray() dump and a Log::debug('wizard.transition', [...]) in your advance() wrapper. When a user reports "it skipped a step," the transition log is the only way to reconstruct which branch fired.composer.json for php: ^7.x and whether it uses each()/dynamic properties; these are the two failures that surface first on PHP 8.2+.StepResolver/factory (register in the container), and storage (bind an interface). Validation and rendering are almost certainly not extension points — wire those in your controller layer instead of patching the vendor tree.src/ is under ~500 lines and has no tests for branching, skip the package and write the three classes yourself. You will spend more time adapting a Symfony-shaped, unmaintained widget than writing a Wizard value object, a Step interface, and a session-backed store.How can I help you explore Laravel packages today?