Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Wizard Annotation Bundle Laravel Package

ibrows/wizard-annotation-bundle

A Symfony bundle that brings annotation-driven wizard flows to your application. Declare multi-step form sequences in controller annotations, reducing boilerplate and keeping step logic organized, readable, and easy to maintain. Ideal for guided forms and checkout-style processes.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check before you composer require anything: this is a Symfony bundle, not a Laravel package. It hooks kernel.controller via the EventDispatcher, reads Doctrine annotations off controller methods, and expects your controller to extend (or mix in) its abstract wizard controller. There is no service provider, no facade, no Laravel bridge. If you are on Laravel, the only thing you can harvest from this repo is the pattern — gate step N on the successful validation of steps 1..N-1, tracked in the session — which you would reimplement in a form request + middleware in about 40 lines. On Symfony it is directly usable, but treat it as vendored source, not a dependency: 0 stars, 0 dependents, no ecosystem around it.

Minimal setup on Symfony:

  1. composer require ibrows/wizard-annotation-bundle
  2. Register the bundle in AppKernel/bundles.php, enable framework.annotations (or doctrine/annotations).
  3. Import the bundle's routing if the listener redirects by route name in your version.
  4. Make your controller extend the bundle's abstract wizard controller so the handler is injected.

Read three files first, in this order — they are the entire package:

  • Annotation/Step.php — the annotation class and its options (name, number, validate, and whatever redirect hooks your version exposes).
  • The kernel.controller listener — this is the actual brain; it is where "should I redirect the user back to step 2?" is decided.
  • The wizard handler service — session-backed getCurrentStepNumber() / setCurrentStepNumber(). That is the whole state model.

First use case — a three-step checkout:

use Ibrows\Bundle\WizardAnnotationBundle\Annotation\Step;

/**
 * @Step(name="account", number=1)
 */
public function accountAction(Request $request)
{
    $form = $this->createForm(AccountType::class);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $this->getWizardHandler()->setCurrentStepNumber(2);

        return $this->redirectToRoute('checkout_shipping');
    }

    return $this->render('checkout/account.html.twig', ['form' => $form->createView()]);
}

/**
 * @Step(name="shipping", number=2, validate="validateAccountStep")
 */
public function shippingAction(Request $request) { /* ... */ }

private function validateAccountStep(): bool
{
    return null !== $this->get('session')->get('checkout.account');
}

Implementation Patterns

One controller per wizard. The session key is derived from the controller, not from the annotation name, so two wizards sharing a controller will stomp on each other's step counter. If you need a second flow, add a second controller.

Validation methods must be pure reads. The validate callback runs on every request that the listener intercepts, including GETs to steps the user hasn't reached. Keep them to "does this session/entity state exist and is it complete" — never mutate, never redirect, never throw. A validator with a side effect fires many times per user journey.

POST, then advance, then redirect. Only bump setCurrentStepNumber() after $form->isValid(). Always follow the bump with a redirect so the listener doesn't re-gate the same request. This is plain PRG, but the bundle makes it load-bearing: skipping the redirect means the annotation for the current step gets evaluated against a step number you just changed.

Jump, don't walk. For final submission, setCurrentStepNumber(1) and clear your draft data rather than trusting the user to navigate away. Otherwise the same session re-enters the wizard at step 3 on the next visit.

Branching wizards. Hardcoded number=3 in a redirectToRoute is where these flows rot. Compute the next step in the controller (e.g. a nextStepAfter($current) helper on a small service) so optional steps can be skipped without editing annotations.

Backwards navigation is free, forwards is gated. Let users click back to any earlier step; the listener only blocks forward movement into a step whose predecessor's validator returns false. Render your step nav accordingly — showing a clickable "Step 3" that immediately bounces is the most common UX complaint.

Where the data lives. The bundle tracks the counter, nothing else. Store the collected data yourself — a namespaced session bag for short flows, a draft entity with a completedSteps column if the wizard can be resumed across devices. The draft entity is the better call once the flow exceeds ~5 fields per step.

If you're reimplementing this in Laravel: one WizardState session value keyed per flow, a EnsureStepIsReachable middleware on the step routes, and a finalizeStep() call in each controller after $request->validate(). You get the same behaviour with two classes and no annotation reader.

Gotchas and Tips

Annotations are the trapdoor. This bundle is annotation-driven, and annotations are deprecated in PHP 8 / Symfony 5+ in favour of attributes. Verify whether your version reads attributes; at 0 stars and no activity, it almost certainly does not. On a modern Symfony you are looking at a fork or a rewrite — budget for that before you start.

Extending the right base class is silent-failure territory. The listener looks for the wizard handler on the controller instance. If you forget the base class or wire the trait wrong, you don't get an exception — you get a wizard that never gates. First debugging step is always "is the handler actually present on the controller?"

Redirect loops mean duplicate or gapped step numbers. The listener resolves to the lowest-numbered step it considers incomplete. Two methods with number=2, or a gap at 2 with methods at 1 and 3, produces an infinite bounce between two routes. Dump the annotation metadata and the session step key side by side.

Typo'd validation method names are quiet. The callback is resolved by string. A missing method is treated as not-validated at best, and throws only in debug at worst — which means it works in dev and silently misbehaves in prod. Assert the method exists the first time you run the flow in prod config.

Check what the listener hooks. Gate decisions typically run on master requests only; sub-requests (embedded controllers, ESI fragments) bypass the check, and a direct POST to step 3 may slip past the gate entirely. Never let the annotation be your only ordering guarantee — re-check in the action that the prior step is complete.

Session fixation and shared sessions. The counter lives in the session. Logging out without clearing it, or sharing a session across tabs, re-enters the wizard mid-flight. Clear explicitly on logout and on final submit.

Extension points worth knowing: the handler and the listener are both DI services and can be decorated or replaced. The session store is the piece you'd swap first if you outgrow it — an interface-shaped subclass over your draft entity buys you resumable wizards.

Debugging: add a dev-only kernel.controller subscriber with a priority below the bundle's listener that logs the resolved step number, the matched annotation, and the validator result. Because the whole mechanism is one listener, the log line tells you exactly why you were redirected, and you will need it.

Read the listener before you rely on it. It's a few hundred lines. Vendoring it into src/ with your own tests is more honest than pinning it in composer.json and pretending it's maintained.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings