Reusable Multi-Step Flows with Laravel Controllers
This guide shows you how to build reusable multi-step flows (like wizards or checkout processes) in Laravel using controllers. You’ll learn a clean, session-based pattern that keeps each step in its own method, validates progressively, and lets you reuse the same flow across different parts of your app. You’ll need a basic Laravel project (8.x or later) and familiarity with routes, controllers, and Blade views.
What You’ll Need
- A Laravel application (version 8 or newer recommended)
- Basic understanding of Laravel routing, controllers, and middleware
- Blade templating basics for building step views
- Composer installed for dependency management
Step-by-Step
- Plan your flow. Decide how many steps you need and what data each collects. For example, a checkout flow might have: Step 1 – Contact info, Step 2 – Shipping address, Step 3 – Payment, Step 4 – Review. Keep each step focused on a single responsibility.
- Create a dedicated controller. Run
php artisan make:controller CheckoutController(or a name that fits your flow). This controller will handle all steps of the flow. - Define a session key. At the top of the controller, define a constant like
private const SESSION_KEY = 'checkout_flow';. This will store all data collected across steps in the session. - Create a method for each step. For each step, create a method that shows the form (GET) and another that processes the submission (POST). For example,
contact()andpostContact(). The GET method initializes the session with default values if it doesn’t exist, then renders the step view. The POST method validates the input, stores it in the session, and redirects to the next step. - Implement the GET method. In the GET method, retrieve existing data from the session (if any) and pass it to the view so the user can edit previously entered values. Use
session()->get(self::SESSION_KEY, [])to get the array, then merge with defaults. - Implement the POST method. Validate the incoming request using Laravel’s validation. If validation fails, redirect back with errors and old input. If it passes, store the validated data into the session array:
session()->put(self::SESSION_KEY . '.contact', $validated). Then redirect to the next step’s route. - Create a progress tracker. To make the flow reusable and stateful, store the current step in the session as well. For example,
session()->put(self::SESSION_KEY . '.current_step', 'shipping'). This allows you to enforce step order and prevent skipping. - Add a step guard. In each GET method, check if the previous step is completed. If not, redirect to the first incomplete step. This prevents users from jumping ahead via URL manipulation.
- Build the views. Create Blade views for each step. Use a layout that shows a step indicator (e.g., a progress bar) and a form that posts to the corresponding POST route. Include a hidden field or use the route name to track the current step.
- Handle final submission. The last step’s POST method should gather all data from the session, perform any final processing (e.g., save to database, send email), then clear the session key and redirect to a success page.
- Make it reusable. To reuse the same flow in different contexts (e.g., for guests vs. logged-in users), extract the flow logic into a base class or trait. You can also pass parameters to the controller methods to customize behavior, like a different redirect after completion.
- Add a cancel/reset route. Provide a way to abandon the flow and clear the session data. A simple route that calls
session()->forget(self::SESSION_KEY)and redirects away.
Common Mistakes
- Not validating step-by-step. Don’t validate everything at the end; validate each step as it’s submitted to give immediate feedback.
- Forgetting to merge session data. When showing a step, always merge existing session data with defaults, otherwise the form will appear empty even if the user already filled it.
- Storing sensitive data in session. Avoid storing credit card numbers or passwords in the session; use temporary tokens or process them immediately and discard.
- Not clearing the session after completion. If you don’t clear the session, the next time the user starts the flow they’ll see old data.
- Hard-coding step URLs. Use named routes to make the flow maintainable and easier to change.
Tips
- Use route model binding for resources. If your flow involves creating or editing a model, pass the model ID as a route parameter and store it in the session for later steps.
- Create a form request class for each step. This keeps validation rules organized and reusable.
- Use a step enum or constant array. Define the list of steps in one place to avoid typos and make it easy to add/remove steps.
- Consider using a package. There are packages like
spatie/laravel-multitenancyorrappasoft/laravel-livewirethat offer wizard components, but the controller approach is lightweight and framework-native. - Test each step. Write feature tests that simulate completing each step and verify the session data is stored correctly.
Troubleshooting
- User gets stuck on a step. Check that the previous step’s POST method actually stores the data and redirects correctly. Use
dd(session()->all())to inspect the session. - Validation errors not showing. Make sure your view includes
@error('field')directives and that the redirect back includes errors (it does by default). Also check that the form method is POST and the route is correct. - Session data lost between requests. Ensure your routes are in the
webmiddleware group, which includes the session middleware. Also, avoid callingsession()->flush()anywhere else. - Step guard blocking legitimate access. If you’re using a step guard, make sure you’re checking the correct session key and that the previous step actually sets a flag. Test by manually setting the session data.
- Reusing the flow in multiple places. If you need different behavior, pass a configuration array to the controller or use a factory method to create the controller with specific options.
Frequently Asked Questions
Can I use this pattern for a multi-page form without a wizard look?
Yes, the pattern works for any multi-step form, even if you don’t show a progress bar. The session storage and step-by-step validation are independent of the UI.
How do I handle file uploads in a step?
Store the uploaded file temporarily (e.g., in storage/app/tmp) and save its path in the session. On final submission, move it to its permanent location.
Is it safe to store sensitive data like payment info in the session?
Generally no. For payment info, use a payment gateway that tokenizes the data and store only the token. For other sensitive data, consider encrypting the session or using a temporary storage with expiration.
Can I make the flow reusable for different models?
Yes, by making the controller accept a model type or by using a trait that can be included in different controllers. The session key can be dynamic based on the model.
What if the user goes back to a previous step and changes data?
That’s fine. The POST method for that step will overwrite the session data for that step. Just ensure that the step guard allows going back (it should, as long as the previous steps are complete).
How do I handle the browser back button?
Use the step guard to check the current step in the session. If the user hits back, they’ll see the previous step’s form with the data they entered. The guard will still allow them to proceed forward.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.