{"id":646,"date":"2025-09-05T11:47:40","date_gmt":"2025-09-05T11:47:40","guid":{"rendered":"https:\/\/1v0.net\/blog\/?p=646"},"modified":"2025-09-05T11:47:44","modified_gmt":"2025-09-05T11:47:44","slug":"building-a-multi-step-form-wizard-in-laravel","status":"publish","type":"post","link":"https:\/\/1v0.net\/blog\/building-a-multi-step-form-wizard-in-laravel\/","title":{"rendered":"Building a Multi-Step Form Wizard in Laravel"},"content":{"rendered":"\n<p>Multi-step (\u201cwizard\u201d) forms improve completion rates by breaking long forms into smaller, focused steps. In this guide, you\u2019ll build a robust Laravel multi-step form with <strong>session-backed state<\/strong>, per-step <strong>validation<\/strong>, guarded navigation (no step skipping), and a clean <strong>Blade UI<\/strong> including a progress indicator. We\u2019ll also cover optional file handling, edit mode, and testing hooks so your wizard is reliable in production.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Routes &amp; Controller Skeleton<\/strong><\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ routes\/web.php<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Controllers<\/span>\\<span class=\"hljs-title\">RegistrationWizardController<\/span>;\n\nRoute::prefix(<span class=\"hljs-string\">'register-wizard'<\/span>)-&gt;name(<span class=\"hljs-string\">'wizard.'<\/span>)-&gt;group(<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">()<\/span> <\/span>{\n    Route::get(<span class=\"hljs-string\">'\/step\/{step}'<\/span>, &#91;RegistrationWizardController::class, <span class=\"hljs-string\">'show'<\/span>])-&gt;name(<span class=\"hljs-string\">'show'<\/span>);\n    Route::post(<span class=\"hljs-string\">'\/step\/{step}'<\/span>, &#91;RegistrationWizardController::class, <span class=\"hljs-string\">'store'<\/span>])-&gt;name(<span class=\"hljs-string\">'store'<\/span>);\n    Route::post(<span class=\"hljs-string\">'\/back\/{step}'<\/span>, &#91;RegistrationWizardController::class, <span class=\"hljs-string\">'back'<\/span>])-&gt;name(<span class=\"hljs-string\">'back'<\/span>);\n    Route::post(<span class=\"hljs-string\">'\/reset'<\/span>, &#91;RegistrationWizardController::class, <span class=\"hljs-string\">'reset'<\/span>])-&gt;name(<span class=\"hljs-string\">'reset'<\/span>);\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We\u2019ll use a single controller with dynamic step routes to keep the flow predictable. Each step has its own GET for display and POST for validation + persistence to session. A <em>back<\/em> action supports safe navigation without losing data, and <em>reset<\/em> clears the flow.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Http\/Controllers\/RegistrationWizardController.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Controllers<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Requests<\/span>\\<span class=\"hljs-title\">Wizard<\/span>\\<span class=\"hljs-title\">StepOneRequest<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Requests<\/span>\\<span class=\"hljs-title\">Wizard<\/span>\\<span class=\"hljs-title\">StepTwoRequest<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Requests<\/span>\\<span class=\"hljs-title\">Wizard<\/span>\\<span class=\"hljs-title\">StepThreeRequest<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Request<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">RegistrationWizardController<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">Controller<\/span>\n<\/span>{\n    <span class=\"hljs-comment\">\/\/ Define the ordered steps for guard checks<\/span>\n    <span class=\"hljs-keyword\">private<\/span> <span class=\"hljs-keyword\">array<\/span> $steps = &#91;<span class=\"hljs-string\">'1'<\/span>, <span class=\"hljs-string\">'2'<\/span>, <span class=\"hljs-string\">'3'<\/span>, <span class=\"hljs-string\">'confirm'<\/span>];\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">show<\/span><span class=\"hljs-params\">(Request $request, string $step)<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">$this<\/span>-&gt;guardStep($request, $step);\n\n        <span class=\"hljs-keyword\">return<\/span> view(<span class=\"hljs-string\">'wizard.step-'<\/span>.$step, &#91;\n            <span class=\"hljs-string\">'data'<\/span> =&gt; $request-&gt;session()-&gt;get(<span class=\"hljs-string\">'wizard'<\/span>, &#91;]),\n            <span class=\"hljs-string\">'current'<\/span> =&gt; $step,\n            <span class=\"hljs-string\">'steps'<\/span> =&gt; <span class=\"hljs-keyword\">$this<\/span>-&gt;steps,\n        ]);\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">store<\/span><span class=\"hljs-params\">(Request $request, string $step)<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">$this<\/span>-&gt;guardStep($request, $step);\n\n        $validated = match ($step) {\n            <span class=\"hljs-string\">'1'<\/span> =&gt; app(StepOneRequest::class)-&gt;validated(),\n            <span class=\"hljs-string\">'2'<\/span> =&gt; app(StepTwoRequest::class)-&gt;validated(),\n            <span class=\"hljs-string\">'3'<\/span> =&gt; app(StepThreeRequest::class)-&gt;validated(),\n            <span class=\"hljs-string\">'confirm'<\/span> =&gt; &#91;],\n            <span class=\"hljs-keyword\">default<\/span> =&gt; abort(<span class=\"hljs-number\">404<\/span>),\n        };\n\n        <span class=\"hljs-comment\">\/\/ Merge this step's validated data into the session \"wizard\" payload<\/span>\n        $payload = array_merge(\n            $request-&gt;session()-&gt;get(<span class=\"hljs-string\">'wizard'<\/span>, &#91;]),\n            &#91;$step =&gt; $validated]\n        );\n        $request-&gt;session()-&gt;put(<span class=\"hljs-string\">'wizard'<\/span>, $payload);\n\n        <span class=\"hljs-comment\">\/\/ When user confirms, persist &amp; finish<\/span>\n        <span class=\"hljs-keyword\">if<\/span> ($step === <span class=\"hljs-string\">'confirm'<\/span>) {\n            <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">$this<\/span>-&gt;finish($request);\n        }\n\n        <span class=\"hljs-comment\">\/\/ Advance to next step<\/span>\n        $next = <span class=\"hljs-keyword\">$this<\/span>-&gt;nextStep($step);\n        <span class=\"hljs-keyword\">return<\/span> redirect()-&gt;route(<span class=\"hljs-string\">'wizard.show'<\/span>, &#91;<span class=\"hljs-string\">'step'<\/span> =&gt; $next]);\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">back<\/span><span class=\"hljs-params\">(Request $request, string $step)<\/span>\n    <\/span>{\n        $prev = <span class=\"hljs-keyword\">$this<\/span>-&gt;previousStep($step);\n        <span class=\"hljs-keyword\">return<\/span> redirect()-&gt;route(<span class=\"hljs-string\">'wizard.show'<\/span>, &#91;<span class=\"hljs-string\">'step'<\/span> =&gt; $prev]);\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">reset<\/span><span class=\"hljs-params\">(Request $request)<\/span>\n    <\/span>{\n        $request-&gt;session()-&gt;forget(<span class=\"hljs-string\">'wizard'<\/span>);\n        <span class=\"hljs-keyword\">return<\/span> redirect()-&gt;route(<span class=\"hljs-string\">'wizard.show'<\/span>, &#91;<span class=\"hljs-string\">'step'<\/span> =&gt; <span class=\"hljs-string\">'1'<\/span>]);\n    }\n\n    <span class=\"hljs-keyword\">private<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">finish<\/span><span class=\"hljs-params\">(Request $request)<\/span>\n    <\/span>{\n        $all = $request-&gt;session()-&gt;get(<span class=\"hljs-string\">'wizard'<\/span>, &#91;]);\n\n        <span class=\"hljs-comment\">\/\/ Example: flatten and persist into your domain model(s)<\/span>\n        $data = array_merge($all&#91;<span class=\"hljs-string\">'1'<\/span>] ?? &#91;], $all&#91;<span class=\"hljs-string\">'2'<\/span>] ?? &#91;], $all&#91;<span class=\"hljs-string\">'3'<\/span>] ?? &#91;]);\n        <span class=\"hljs-comment\">\/\/ \\App\\Models\\User::create($data); \/\/ adapt for your case<\/span>\n\n        $request-&gt;session()-&gt;forget(<span class=\"hljs-string\">'wizard'<\/span>);\n\n        <span class=\"hljs-keyword\">return<\/span> redirect(<span class=\"hljs-string\">'\/'<\/span>)-&gt;with(<span class=\"hljs-string\">'success'<\/span>, <span class=\"hljs-string\">'Registration complete!'<\/span>);\n    }\n\n    <span class=\"hljs-keyword\">private<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">guardStep<\/span><span class=\"hljs-params\">(Request $request, string $step)<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        abort_unless(in_array($step, <span class=\"hljs-keyword\">$this<\/span>-&gt;steps, <span class=\"hljs-keyword\">true<\/span>), <span class=\"hljs-number\">404<\/span>);\n\n        <span class=\"hljs-comment\">\/\/ Prevent skipping ahead: ensure all prior steps exist in session<\/span>\n        $idx = array_search($step, <span class=\"hljs-keyword\">$this<\/span>-&gt;steps, <span class=\"hljs-keyword\">true<\/span>);\n        <span class=\"hljs-keyword\">for<\/span> ($i = <span class=\"hljs-number\">0<\/span>; $i &lt; $idx; $i++) {\n            $required = <span class=\"hljs-keyword\">$this<\/span>-&gt;steps&#91;$i];\n            <span class=\"hljs-keyword\">if<\/span> ($required !== <span class=\"hljs-string\">'confirm'<\/span> &amp;&amp; <span class=\"hljs-keyword\">empty<\/span>($request-&gt;session()-&gt;get(<span class=\"hljs-string\">'wizard.'<\/span>.$required))) {\n                redirect()-&gt;route(<span class=\"hljs-string\">'wizard.show'<\/span>, &#91;<span class=\"hljs-string\">'step'<\/span> =&gt; $required])-&gt;send();\n                <span class=\"hljs-keyword\">exit<\/span>;\n            }\n        }\n    }\n\n    <span class=\"hljs-keyword\">private<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">nextStep<\/span><span class=\"hljs-params\">(string $step)<\/span>: <span class=\"hljs-title\">string<\/span>\n    <\/span>{\n        $i = array_search($step, <span class=\"hljs-keyword\">$this<\/span>-&gt;steps, <span class=\"hljs-keyword\">true<\/span>);\n        <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">$this<\/span>-&gt;steps&#91;min($i + <span class=\"hljs-number\">1<\/span>, count(<span class=\"hljs-keyword\">$this<\/span>-&gt;steps) - <span class=\"hljs-number\">1<\/span>)];\n    }\n\n    <span class=\"hljs-keyword\">private<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">previousStep<\/span><span class=\"hljs-params\">(string $step)<\/span>: <span class=\"hljs-title\">string<\/span>\n    <\/span>{\n        $i = array_search($step, <span class=\"hljs-keyword\">$this<\/span>-&gt;steps, <span class=\"hljs-keyword\">true<\/span>);\n        <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">$this<\/span>-&gt;steps&#91;max($i - <span class=\"hljs-number\">1<\/span>, <span class=\"hljs-number\">0<\/span>)];\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><strong>Key ideas:<\/strong> session stores each step\u2019s validated subset; <em>guardStep()<\/em> blocks direct navigation to later steps; <em>finish()<\/em> consolidates session data into your database and clears the wizard state.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Per-Step Validation with Form Requests<\/strong><\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">php artisan make:request Wizard\/StepOneRequest\nphp artisan make:request Wizard\/StepTwoRequest\nphp artisan make:request Wizard\/StepThreeRequest<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Using dedicated <em>FormRequest<\/em> classes keeps validation clean and testable. Each step validates only its own fields and messages.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Http\/Requests\/Wizard\/StepOneRequest.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">Requests<\/span>\\<span class=\"hljs-title\">Wizard<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Foundation<\/span>\\<span class=\"hljs-title\">Http<\/span>\\<span class=\"hljs-title\">FormRequest<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">StepOneRequest<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">FormRequest<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">authorize<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">bool<\/span> <\/span>{ <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">true<\/span>; }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">rules<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">array<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">return<\/span> &#91;\n            <span class=\"hljs-string\">'first_name'<\/span> =&gt; &#91;<span class=\"hljs-string\">'required'<\/span>,<span class=\"hljs-string\">'string'<\/span>,<span class=\"hljs-string\">'max:80'<\/span>],\n            <span class=\"hljs-string\">'last_name'<\/span>  =&gt; &#91;<span class=\"hljs-string\">'required'<\/span>,<span class=\"hljs-string\">'string'<\/span>,<span class=\"hljs-string\">'max:80'<\/span>],\n            <span class=\"hljs-string\">'email'<\/span>      =&gt; &#91;<span class=\"hljs-string\">'required'<\/span>,<span class=\"hljs-string\">'email'<\/span>,<span class=\"hljs-string\">'max:255'<\/span>],\n        ];\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Http\/Requests\/Wizard\/StepTwoRequest.php<\/span>\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">StepTwoRequest<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">FormRequest<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">authorize<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">bool<\/span> <\/span>{ <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">true<\/span>; }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">rules<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">array<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">return<\/span> &#91;\n            <span class=\"hljs-string\">'address'<\/span> =&gt; &#91;<span class=\"hljs-string\">'required'<\/span>,<span class=\"hljs-string\">'string'<\/span>,<span class=\"hljs-string\">'max:255'<\/span>],\n            <span class=\"hljs-string\">'city'<\/span>    =&gt; &#91;<span class=\"hljs-string\">'required'<\/span>,<span class=\"hljs-string\">'string'<\/span>,<span class=\"hljs-string\">'max:100'<\/span>],\n            <span class=\"hljs-string\">'country'<\/span> =&gt; &#91;<span class=\"hljs-string\">'required'<\/span>,<span class=\"hljs-string\">'string'<\/span>,<span class=\"hljs-string\">'max:2'<\/span>], <span class=\"hljs-comment\">\/\/ ISO code<\/span>\n        ];\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Http\/Requests\/Wizard\/StepThreeRequest.php<\/span>\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">StepThreeRequest<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">FormRequest<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">authorize<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">bool<\/span> <\/span>{ <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">true<\/span>; }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">rules<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">array<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">return<\/span> &#91;\n            <span class=\"hljs-string\">'password'<\/span> =&gt; &#91;<span class=\"hljs-string\">'required'<\/span>,<span class=\"hljs-string\">'confirmed'<\/span>,<span class=\"hljs-string\">'min:8'<\/span>],\n            <span class=\"hljs-string\">'terms'<\/span>    =&gt; &#91;<span class=\"hljs-string\">'accepted'<\/span>],\n        ];\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Each step populates only its slice of the session payload, reducing coupling and improving clarity in error states.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Blade UI: Layout, Progress Bar, and Step Views<\/strong><\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;!-- resources\/views\/layouts\/wizard.blade.php --&gt;\n&lt;!DOCTYPE html&gt;\n&lt;html lang=<span class=\"hljs-string\">\"en\"<\/span>&gt;\n&lt;head&gt;\n    &lt;meta charset=<span class=\"hljs-string\">\"UTF-8\"<\/span>&gt;\n    &lt;meta name=<span class=\"hljs-string\">\"viewport\"<\/span> content=<span class=\"hljs-string\">\"width=device-width, initial-scale=1.0\"<\/span>&gt;\n    &lt;title&gt;Multi-Step Wizard&lt;\/title&gt;\n    &lt;style&gt;\n        .progress { display:flex; gap:<span class=\"hljs-number\">.5<\/span>rem; margin-bottom:<span class=\"hljs-number\">1<\/span>rem; }\n        .dot { width:<span class=\"hljs-number\">12<\/span>px; height:<span class=\"hljs-number\">12<\/span>px; border-radius:<span class=\"hljs-number\">999<\/span>px; background:<span class=\"hljs-comment\">#ddd; }<\/span>\n        .dot.active { background:<span class=\"hljs-comment\">#4f46e5; }<\/span>\n        .step-card { max-width:<span class=\"hljs-number\">720<\/span>px; margin:auto; padding:<span class=\"hljs-number\">1.25<\/span>rem; border:<span class=\"hljs-number\">1<\/span>px solid <span class=\"hljs-comment\">#eee; border-radius:12px; }<\/span>\n        .actions { display:flex; gap:<span class=\"hljs-number\">.5<\/span>rem; margin-top:<span class=\"hljs-number\">1<\/span>rem; }\n        .error { color:<span class=\"hljs-comment\">#b91c1c; font-size:.9rem; }<\/span>\n    &lt;\/style&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n    &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">progress<\/span>\"&gt;\n        @<span class=\"hljs-title\">foreach<\/span>($<span class=\"hljs-title\">steps<\/span> <span class=\"hljs-title\">as<\/span> $<span class=\"hljs-title\">s<\/span>)\n            &lt;<span class=\"hljs-title\">div<\/span> <span class=\"hljs-title\">class<\/span>=\"<span class=\"hljs-title\">dot<\/span> <\/span>{{ $s === $current ? <span class=\"hljs-string\">'active'<\/span> : <span class=\"hljs-string\">''<\/span> }}<span class=\"hljs-string\">\" title=\"<\/span>Step {{ $s }}<span class=\"hljs-string\">\"&gt;&lt;\/div&gt;\n        @endforeach\n    &lt;\/div&gt;\n\n    &lt;div class=\"<\/span>step-card<span class=\"hljs-string\">\"&gt;\n        @yield('content')\n    &lt;\/div&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>A minimal layout with a <em>dot-based<\/em> progress bar communicates the user\u2019s position without heavy CSS frameworks. Feel free to swap with Tailwind or Bootstrap if your project already uses them.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;!-- resources\/views\/wizard\/step<span class=\"hljs-number\">-1.<\/span>blade.php --&gt;\n@extends(<span class=\"hljs-string\">'layouts.wizard'<\/span>)\n\n@section(<span class=\"hljs-string\">'content'<\/span>)\n&lt;h2&gt;Step <span class=\"hljs-number\">1<\/span>: Your Details&lt;\/h2&gt;\n\n&lt;form method=<span class=\"hljs-string\">\"POST\"<\/span> action=<span class=\"hljs-string\">\"{{ route('wizard.store',&#91;'step' =&gt; '1']) }}\"<\/span>&gt;\n    @csrf\n    &lt;label&gt;First name&lt;\/label&gt;\n    &lt;input name=<span class=\"hljs-string\">\"first_name\"<\/span> value=<span class=\"hljs-string\">\"{{ old('first_name', $data&#91;'1']&#91;'first_name'] ?? '') }}\"<\/span> \/&gt;\n    @error(<span class=\"hljs-string\">'first_name'<\/span>) &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">error<\/span>\"&gt;<\/span>{{ $message }}&lt;\/div&gt; @enderror\n\n    &lt;label&gt;Last name&lt;\/label&gt;\n    &lt;input name=<span class=\"hljs-string\">\"last_name\"<\/span> value=<span class=\"hljs-string\">\"{{ old('last_name', $data&#91;'1']&#91;'last_name'] ?? '') }}\"<\/span> \/&gt;\n    @error(<span class=\"hljs-string\">'last_name'<\/span>) &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">error<\/span>\"&gt;<\/span>{{ $message }}&lt;\/div&gt; @enderror\n\n    &lt;label&gt;Email&lt;\/label&gt;\n    &lt;input name=<span class=\"hljs-string\">\"email\"<\/span> type=<span class=\"hljs-string\">\"email\"<\/span> value=<span class=\"hljs-string\">\"{{ old('email', $data&#91;'1']&#91;'email'] ?? '') }}\"<\/span> \/&gt;\n    @error(<span class=\"hljs-string\">'email'<\/span>) &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">error<\/span>\"&gt;<\/span>{{ $message }}&lt;\/div&gt; @enderror\n\n    &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">actions<\/span>\"&gt;\n        &lt;<span class=\"hljs-title\">button<\/span> <span class=\"hljs-title\">type<\/span>=\"<span class=\"hljs-title\">submit<\/span>\"&gt;<span class=\"hljs-title\">Next<\/span>&lt;\/<span class=\"hljs-title\">button<\/span>&gt;\n        &lt;<span class=\"hljs-title\">form<\/span> <span class=\"hljs-title\">method<\/span>=\"<span class=\"hljs-title\">POST<\/span>\" <span class=\"hljs-title\">action<\/span>=\"<\/span>{{ route(<span class=\"hljs-string\">'wizard.reset'<\/span>) }}<span class=\"hljs-string\">\"&gt; @csrf &lt;button&gt;Reset&lt;\/button&gt; &lt;\/form&gt;\n    &lt;\/div&gt;\n&lt;\/form&gt;\n@endsection<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;!-- resources\/views\/wizard\/step<span class=\"hljs-number\">-2.<\/span>blade.php --&gt;\n@extends(<span class=\"hljs-string\">'layouts.wizard'<\/span>)\n\n@section(<span class=\"hljs-string\">'content'<\/span>)\n&lt;h2&gt;Step <span class=\"hljs-number\">2<\/span>: Address&lt;\/h2&gt;\n\n&lt;form method=<span class=\"hljs-string\">\"POST\"<\/span> action=<span class=\"hljs-string\">\"{{ route('wizard.store',&#91;'step' =&gt; '2']) }}\"<\/span>&gt;\n    @csrf\n    &lt;label&gt;Address&lt;\/label&gt;\n    &lt;input name=<span class=\"hljs-string\">\"address\"<\/span> value=<span class=\"hljs-string\">\"{{ old('address', $data&#91;'2']&#91;'address'] ?? '') }}\"<\/span> \/&gt;\n    @error(<span class=\"hljs-string\">'address'<\/span>) &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">error<\/span>\"&gt;<\/span>{{ $message }}&lt;\/div&gt; @enderror\n\n    &lt;label&gt;City&lt;\/label&gt;\n    &lt;input name=<span class=\"hljs-string\">\"city\"<\/span> value=<span class=\"hljs-string\">\"{{ old('city', $data&#91;'2']&#91;'city'] ?? '') }}\"<\/span> \/&gt;\n    @error(<span class=\"hljs-string\">'city'<\/span>) &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">error<\/span>\"&gt;<\/span>{{ $message }}&lt;\/div&gt; @enderror\n\n    &lt;label&gt;Country (ISO)&lt;\/label&gt;\n    &lt;input name=<span class=\"hljs-string\">\"country\"<\/span> maxlength=<span class=\"hljs-string\">\"2\"<\/span> value=<span class=\"hljs-string\">\"{{ old('country', $data&#91;'2']&#91;'country'] ?? '') }}\"<\/span> \/&gt;\n    @error(<span class=\"hljs-string\">'country'<\/span>) &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">error<\/span>\"&gt;<\/span>{{ $message }}&lt;\/div&gt; @enderror\n\n    &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">actions<\/span>\"&gt;\n        &lt;<span class=\"hljs-title\">form<\/span> <span class=\"hljs-title\">method<\/span>=\"<span class=\"hljs-title\">POST<\/span>\" <span class=\"hljs-title\">action<\/span>=\"<\/span>{{ route(<span class=\"hljs-string\">'wizard.back'<\/span>,&#91;<span class=\"hljs-string\">'step'<\/span> =&gt; <span class=\"hljs-string\">'2'<\/span>]) }}<span class=\"hljs-string\">\"&gt; @csrf &lt;button&gt;Back&lt;\/button&gt; &lt;\/form&gt;\n        &lt;button type=\"<\/span>submit<span class=\"hljs-string\">\"&gt;Next&lt;\/button&gt;\n    &lt;\/div&gt;\n&lt;\/form&gt;\n@endsection<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;!-- resources\/views\/wizard\/step<span class=\"hljs-number\">-3.<\/span>blade.php --&gt;\n@extends(<span class=\"hljs-string\">'layouts.wizard'<\/span>)\n\n@section(<span class=\"hljs-string\">'content'<\/span>)\n&lt;h2&gt;Step <span class=\"hljs-number\">3<\/span>: Security&lt;\/h2&gt;\n\n&lt;form method=<span class=\"hljs-string\">\"POST\"<\/span> action=<span class=\"hljs-string\">\"{{ route('wizard.store',&#91;'step' =&gt; '3']) }}\"<\/span>&gt;\n    @csrf\n    &lt;label&gt;Password&lt;\/label&gt;\n    &lt;input type=<span class=\"hljs-string\">\"password\"<\/span> name=<span class=\"hljs-string\">\"password\"<\/span> \/&gt;\n    @error(<span class=\"hljs-string\">'password'<\/span>) &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">error<\/span>\"&gt;<\/span>{{ $message }}&lt;\/div&gt; @enderror\n\n    &lt;label&gt;Confirm Password&lt;\/label&gt;\n    &lt;input type=<span class=\"hljs-string\">\"password\"<\/span> name=<span class=\"hljs-string\">\"password_confirmation\"<\/span> \/&gt;\n\n    &lt;label&gt;&lt;input type=<span class=\"hljs-string\">\"checkbox\"<\/span> name=<span class=\"hljs-string\">\"terms\"<\/span> value=<span class=\"hljs-string\">\"1\"<\/span> {{ old(<span class=\"hljs-string\">'terms'<\/span>, $data&#91;<span class=\"hljs-string\">'3'<\/span>]&#91;<span class=\"hljs-string\">'terms'<\/span>] ?? <span class=\"hljs-keyword\">false<\/span>) ? <span class=\"hljs-string\">'checked'<\/span> : <span class=\"hljs-string\">''<\/span> }} \/&gt; I accept terms&lt;\/label&gt;\n    @error(<span class=\"hljs-string\">'terms'<\/span>) &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">error<\/span>\"&gt;<\/span>{{ $message }}&lt;\/div&gt; @enderror\n\n    &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">actions<\/span>\"&gt;\n        &lt;<span class=\"hljs-title\">form<\/span> <span class=\"hljs-title\">method<\/span>=\"<span class=\"hljs-title\">POST<\/span>\" <span class=\"hljs-title\">action<\/span>=\"<\/span>{{ route(<span class=\"hljs-string\">'wizard.back'<\/span>,&#91;<span class=\"hljs-string\">'step'<\/span> =&gt; <span class=\"hljs-string\">'3'<\/span>]) }}<span class=\"hljs-string\">\"&gt; @csrf &lt;button&gt;Back&lt;\/button&gt; &lt;\/form&gt;\n        &lt;button type=\"<\/span>submit<span class=\"hljs-string\">\"&gt;Next&lt;\/button&gt;\n    &lt;\/div&gt;\n&lt;\/form&gt;\n@endsection<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;!-- resources\/views\/wizard\/step-confirm.blade.php --&gt;\n@extends(<span class=\"hljs-string\">'layouts.wizard'<\/span>)\n\n@section(<span class=\"hljs-string\">'content'<\/span>)\n&lt;h2&gt;Confirm &amp; Submit&lt;\/h2&gt;\n\n@php($all = $data ?? &#91;])\n&lt;pre&gt;{{ json_encode($all, JSON_PRETTY_PRINT) }}&lt;\/pre&gt;\n\n&lt;form method=<span class=\"hljs-string\">\"POST\"<\/span> action=<span class=\"hljs-string\">\"{{ route('wizard.store',&#91;'step' =&gt; 'confirm']) }}\"<\/span>&gt;\n    @csrf\n    &lt;div <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">actions<\/span>\"&gt;\n        &lt;<span class=\"hljs-title\">form<\/span> <span class=\"hljs-title\">method<\/span>=\"<span class=\"hljs-title\">POST<\/span>\" <span class=\"hljs-title\">action<\/span>=\"<\/span>{{ route(<span class=\"hljs-string\">'wizard.back'<\/span>,&#91;<span class=\"hljs-string\">'step'<\/span> =&gt; <span class=\"hljs-string\">'confirm'<\/span>]) }}<span class=\"hljs-string\">\"&gt; @csrf &lt;button&gt;Back&lt;\/button&gt; &lt;\/form&gt;\n        &lt;button type=\"<\/span>submit<span class=\"hljs-string\">\"&gt;Confirm&lt;\/button&gt;\n    &lt;\/div&gt;\n&lt;\/form&gt;\n@endsection<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Each view rehydrates fields from session so users don\u2019t lose progress. The confirmation screen shows a summary; in real apps, present a styled review instead of raw JSON.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Preventing Step Skips &amp; Handling Expiry<\/strong><\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ Optionally, add a timestamp to invalidate long-idle sessions<\/span>\n<span class=\"hljs-comment\">\/\/ In store(): after merging payload<\/span>\n$payload&#91;<span class=\"hljs-string\">'_meta'<\/span>]&#91;<span class=\"hljs-string\">'touched_at'<\/span>] = now()-&gt;timestamp;\n$request-&gt;session()-&gt;put(<span class=\"hljs-string\">'wizard'<\/span>, $payload);\n\n<span class=\"hljs-comment\">\/\/ In guardStep(): invalidate after 60 minutes of inactivity<\/span>\n$wizard = $request-&gt;session()-&gt;get(<span class=\"hljs-string\">'wizard'<\/span>, &#91;]);\n<span class=\"hljs-keyword\">if<\/span> (($wizard&#91;<span class=\"hljs-string\">'_meta'<\/span>]&#91;<span class=\"hljs-string\">'touched_at'<\/span>] ?? <span class=\"hljs-number\">0<\/span>) &lt; now()-&gt;subMinutes(<span class=\"hljs-number\">60<\/span>)-&gt;timestamp) {\n    $request-&gt;session()-&gt;forget(<span class=\"hljs-string\">'wizard'<\/span>);\n    redirect()-&gt;route(<span class=\"hljs-string\">'wizard.show'<\/span>, &#91;<span class=\"hljs-string\">'step'<\/span> =&gt; <span class=\"hljs-string\">'1'<\/span>])-&gt;with(<span class=\"hljs-string\">'warning'<\/span>,<span class=\"hljs-string\">'Your session expired.'<\/span>)-&gt;send();\n    <span class=\"hljs-keyword\">exit<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Guard clauses ensure users can\u2019t land on later steps without completing earlier ones. Adding an inactivity expiry avoids stale multi-day flows or abandoned sessions.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Optional: File Uploads in a Step<\/strong><\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ Example rule inside a FormRequest for a file step:<\/span>\n<span class=\"hljs-string\">'avatar'<\/span> =&gt; &#91;<span class=\"hljs-string\">'nullable'<\/span>,<span class=\"hljs-string\">'image'<\/span>,<span class=\"hljs-string\">'mimes:jpg,jpeg,png'<\/span>,<span class=\"hljs-string\">'max:2048'<\/span>];\n\n<span class=\"hljs-comment\">\/\/ Controller store() for that step:<\/span>\n<span class=\"hljs-keyword\">if<\/span> ($request-&gt;hasFile(<span class=\"hljs-string\">'avatar'<\/span>)) {\n    $path = $request-&gt;file(<span class=\"hljs-string\">'avatar'<\/span>)-&gt;store(<span class=\"hljs-string\">'wizard-avatars'<\/span>,<span class=\"hljs-string\">'public'<\/span>);\n    $validated&#91;<span class=\"hljs-string\">'avatar_path'<\/span>] = $path;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>When a step includes files, store them immediately and keep only the path in session (not raw file data). Serve back previews from disk and re-validate on confirmation.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Edit Mode (Reopen Wizard with Existing Data)<\/strong><\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ Preload existing profile data into session then redirect to step 1<\/span>\n<span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">edit<\/span><span class=\"hljs-params\">(Request $request)<\/span>\n<\/span>{\n    $profile = auth()-&gt;user()-&gt;profile;\n    $request-&gt;session()-&gt;put(<span class=\"hljs-string\">'wizard'<\/span>, &#91;\n        <span class=\"hljs-string\">'1'<\/span> =&gt; &#91;<span class=\"hljs-string\">'first_name'<\/span> =&gt; $profile-&gt;first_name, <span class=\"hljs-string\">'last_name'<\/span> =&gt; $profile-&gt;last_name, <span class=\"hljs-string\">'email'<\/span> =&gt; $profile-&gt;email],\n        <span class=\"hljs-string\">'2'<\/span> =&gt; &#91;<span class=\"hljs-string\">'address'<\/span> =&gt; $profile-&gt;address, <span class=\"hljs-string\">'city'<\/span> =&gt; $profile-&gt;city, <span class=\"hljs-string\">'country'<\/span> =&gt; $profile-&gt;country],\n        <span class=\"hljs-string\">'3'<\/span> =&gt; &#91;],\n    ]);\n    <span class=\"hljs-keyword\">return<\/span> redirect()-&gt;route(<span class=\"hljs-string\">'wizard.show'<\/span>, &#91;<span class=\"hljs-string\">'step'<\/span> =&gt; <span class=\"hljs-string\">'1'<\/span>]);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>For editing, hydrate the session with existing values, then let users move through steps and resubmit. On finish, update rather than create new rows.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Feature Testing the Wizard<\/strong><\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ tests\/Feature\/RegistrationWizardTest.php<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Tests<\/span>\\<span class=\"hljs-title\">TestCase<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">RegistrationWizardTest<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">TestCase<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">test_user_cannot_skip_steps<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">$this<\/span>-&gt;get(<span class=\"hljs-string\">'\/register-wizard\/step\/3'<\/span>)-&gt;assertRedirect(<span class=\"hljs-string\">'\/register-wizard\/step\/1'<\/span>);\n    }\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">test_step_one_validates_and_persists<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        <span class=\"hljs-keyword\">$this<\/span>-&gt;post(<span class=\"hljs-string\">'\/register-wizard\/step\/1'<\/span>, &#91;\n            <span class=\"hljs-string\">'first_name'<\/span> =&gt; <span class=\"hljs-string\">'Jane'<\/span>,\n            <span class=\"hljs-string\">'last_name'<\/span>  =&gt; <span class=\"hljs-string\">'Doe'<\/span>,\n            <span class=\"hljs-string\">'email'<\/span>      =&gt; <span class=\"hljs-string\">'jane@example.com'<\/span>,\n        ])-&gt;assertRedirect(<span class=\"hljs-string\">'\/register-wizard\/step\/2'<\/span>);\n\n        <span class=\"hljs-keyword\">$this<\/span>-&gt;get(<span class=\"hljs-string\">'\/register-wizard\/step\/2'<\/span>)-&gt;assertOk();\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Tests should assert redirect behavior on invalid navigation and confirm that each step stores data and advances the flow. Add more cases for expiries and file steps as needed.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Livewire \/ Vue Comparison (When to Choose What)<\/strong><\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Aspect<\/th><th>Blade + Sessions (This Article)<\/th><th>Livewire<\/th><th>Vue 3 (SPA)<\/th><\/tr><\/thead><tbody><tr><td>Complexity<\/td><td>Low<\/td><td>Medium<\/td><td>High<\/td><\/tr><tr><td>State Handling<\/td><td>Server session<\/td><td>Livewire component state<\/td><td>Client store (Pinia\/Vuex)<\/td><\/tr><tr><td>UX Smoothness<\/td><td>Full-page reloads<\/td><td>Partial (AJAX, no reload)<\/td><td>SPA-smooth<\/td><\/tr><tr><td>Best For<\/td><td>Simple to moderate wizards<\/td><td>Dynamic forms without full SPA<\/td><td>Rich UIs, complex flows<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Start with Blade + sessions for speed and simplicity. Move to Livewire for AJAX-powered steps without building an SPA. Choose Vue for complex, highly interactive multi-step flows where client-side routing\/state is a win.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Wrapping Up<\/strong><\/h2>\n\n\n\n<p>You built a production-ready multi-step form wizard using Laravel, Blade, sessions, and per-step FormRequest validation. The guard rules prevent skipping steps; the UI keeps users oriented; and optional features like expiry, file handling, and edit mode make it practical for real apps. Expand with Livewire or Vue when UX demands it.<\/p>\n\n\n\n<div style=\"height:100px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>What\u2019s Next<\/strong><\/h2>\n\n\n\n<p>Keep leveling up your forms and UI:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/blog\/mastering-validation-rules-in-laravel-12\">Mastering Validation Rules in Laravel 12<\/a><\/li>\n\n\n\n<li><a href=\"\/blog\/handling-file-uploads-and-image-storage-in-laravel\">Handling File Uploads and Image Storage in Laravel<\/a><\/li>\n\n\n\n<li><a href=\"\/blog\/how-to-use-laravel-livewire-for-interactive-uis\">How to Use Laravel Livewire for Interactive UIs<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Multi-step (\u201cwizard\u201d) forms improve completion rates by breaking long forms into smaller, focused steps. In this guide, you\u2019ll build a robust Laravel multi-step form with session-backed state, per-step validation, guarded navigation (no step skipping), and a clean Blade UI including a progress indicator. We\u2019ll also cover optional file handling, edit mode, and testing hooks so [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":648,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[96,24,19],"class_list":["post-646","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-laravel","tag-blade","tag-sessions","tag-validation"],"_links":{"self":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/646","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/comments?post=646"}],"version-history":[{"count":1,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/646\/revisions"}],"predecessor-version":[{"id":649,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/646\/revisions\/649"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media\/648"}],"wp:attachment":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media?parent=646"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/categories?post=646"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/tags?post=646"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}