Creating Wizards
Learn how to create multi-step wizards from scratch.
Quick Start
1. Generate a Wizard
php artisan wizard:make Onboarding
Interactive prompts:
What type of wizard do you want to create?
[blade] Blade (Traditional server-side rendering)
[api] API (Headless JSON responses)
[livewire] Livewire (Reactive components)
[inertia] Inertia.js (SPA with Vue/React)
> blade
ℹ Wizard created successfully!
✎ Wizard class: app/Wizards/OnboardingWizard/Onboarding.php
✎ Controller: app/Http/Controllers/OnboardingController.php
✎ Views: resources/views/wizards/onboarding/
Or use command options to skip interactive prompts:
php artisan wizard:make Onboarding --type=api
2. Generate Steps
php artisan wizard:make-step Onboarding PersonalInfo --order=1
Interactive prompts:
What is the step title? › Personal Information
Is this step optional? › No
ℹ Step created successfully!
✎ Step class: app/Wizards/OnboardingWizard/Steps/PersonalInfoStep.php
✎ FormRequest: app/Http/Requests/Wizards/PersonalInfoRequest.php
✎ Step will be auto-discovered
✎ Next steps:
• Add validation rules: app/Http/Requests/Wizards/PersonalInfoRequest.php
• Implement business logic: app/Wizards/OnboardingWizard/Steps/PersonalInfoStep.php
Wizard Types
Laravel Headless Wizard supports 4 wizard types to fit your stack:
Blade Wizards
Traditional server-side rendered wizards with Blade templates and pre-built components.
php artisan wizard:make Onboarding --type=blade
Best for:
- Traditional Laravel applications
- Server-side rendering
- Rapid prototyping with pre-built components
Features:
- Auto-generated Blade views with layout
- Pre-built components (ProgressBar, Navigation, FormWrapper)
- CSRF protection included
- Traditional form submissions
API Wizards
Headless JSON API for modern SPA frameworks (React, Vue, Angular, Svelte).
php artisan wizard:make Onboarding --type=api
Best for:
- Decoupled frontend/backend
- Mobile apps
- Multiple frontend consumers
Features:
- Pure JSON responses
- RESTful API endpoints
- useWizard() Vue composable included
- Requires CSRF exception setup
CSRF Setup Required:
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
$middleware->validateCsrfTokens(except: [
'wizard/onboarding/*',
]);
})
Livewire Wizards
Reactive components with Laravel Livewire.
php artisan wizard:make Onboarding --type=livewire
Best for:
- Reactive UIs without JavaScript frameworks
- Real-time validation
- Dynamic forms
Inertia Wizards
SPA experience with Vue/React using Inertia.js.
php artisan wizard:make Onboarding --type=inertia
Best for:
- Modern SPA with server-side routing
- Vue/React with Laravel backend
- Best of both worlds (SPA + Laravel)
Wizard Structure
A wizard consists of:
- Wizard Class - Orchestrates the overall flow (
app/Wizards/{Name}Wizard/{Name}.php) - Step Classes - Individual wizard steps (
app/Wizards/{Name}Wizard/Steps/) - Form Requests - Laravel validation for each step (
app/Http/Requests/Wizards/) - Auto-Discovery - Wizards are automatically discovered, no config registration needed
Creating Custom Steps
Step Class Example
<?php
namespace App\Wizards\OnboardingWizard\Steps;
use Invelity\WizardPackage\Steps\AbstractStep;
use Invelity\WizardPackage\ValueObjects\StepData;
use Invelity\WizardPackage\ValueObjects\StepResult;
class PersonalInfoStep extends AbstractStep
{
public function __construct()
{
parent::__construct(
id: 'personal-info',
title: 'Personal Information',
order: 1
);
}
public function getFormRequest(): ?string
{
return \App\Http\Requests\Wizards\PersonalInfoRequest::class;
}
public function process(StepData $data): StepResult
{
// Process the step data
$name = $data->get('name');
$email = $data->get('email');
// Your business logic here
// For example, create a user record, send email, etc.
return StepResult::success('Personal information saved!');
}
public function shouldSkip(array $wizardData): bool
{
// Skip this step if email already exists
return isset($wizardData['email_verified']) && $wizardData['email_verified'];
}
public function getDependencies(): array
{
// This step has no dependencies
return [];
}
}
Form Request Example
<?php
namespace App\Http\Requests\Wizards;
use Illuminate\Foundation\Http\FormRequest;
class PersonalInfoRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users,email'],
'phone' => ['nullable', 'string', 'regex:/^[0-9]{10}$/'],
'date_of_birth' => ['required', 'date', 'before:today'],
];
}
public function messages(): array
{
return [
'email.unique' => 'This email is already registered.',
'phone.regex' => 'Phone number must be 10 digits.',
];
}
}
Optional Steps
Make a step optional by passing isOptional: true:
public function __construct()
{
parent::__construct(
id: 'newsletter',
title: 'Newsletter Preferences',
order: 3,
isOptional: true, // Users can skip this step
canSkip: true
);
}
public function getFormRequest(): ?string
{
return null; // No validation for optional step
}
Smart Defaults: Generated step constructors automatically omit
isOptional: falseandcanSkip: falsefor cleaner code. Only include these parameters when set totrue.
Conditional Steps
Skip steps based on wizard data:
public function shouldSkip(array $wizardData): bool
{
// Skip billing if user selected free plan
return $wizardData['plan_type'] === 'free';
}
Step Dependencies
Require other steps to be completed first:
public function getDependencies(): array
{
// This step requires personal-info and address to be completed
return ['personal-info', 'address'];
}
Processing Step Data
The process() method is called after validation:
public function process(StepData $data): StepResult
{
try {
// Access validated data
$name = $data->get('name');
$email = $data->get('email');
// Your business logic
User::create([
'name' => $name,
'email' => $email,
]);
// Return success
return StepResult::success('User created successfully!');
} catch (\Exception $e) {
// Return failure with error message
return StepResult::failure($e->getMessage());
}
}
StepResult Options
// Success
return StepResult::success('Step completed!');
// Success with redirect
return StepResult::redirect('/custom-route', ['key' => 'value']);
// Failure
return StepResult::failure('Something went wrong');
// Failure with validation errors
return StepResult::failure('Validation failed', [
'email' => ['Email already exists'],
]);
Accessing Wizard Data
Get data from previous steps:
use Invelity\WizardPackage\Traits\HasWizardSteps;
class PaymentStep extends AbstractStep
{
use HasWizardSteps;
public function process(StepData $data): StepResult
{
// Get data from previous steps
$userEmail = $this->getWizardData('personal-info.email');
$planType = $this->getWizardData('subscription.plan');
// Process payment
// ...
return StepResult::success();
}
}
Step Lifecycle Events
Listen to step events:
// In your EventServiceProvider
protected $listen = [
\Invelity\WizardPackage\Events\StepCompleted::class => [
SendStepCompletedNotification::class,
],
\Invelity\WizardPackage\Events\StepSkipped::class => [
LogSkippedStep::class,
],
];
Available events:
WizardStarted- When wizard is initializedStepCompleted- When a step is successfully completedStepSkipped- When an optional step is skippedWizardCompleted- When all steps are finished
Blade Components
For Blade wizards, use pre-built components for rapid development:
Layout Component
Provides base wizard layout with title and content slot:
<x-wizard::layout title="User Onboarding">
<!-- Your wizard content here -->
</x-wizard::layout>
Progress Bar Component
Shows wizard completion progress:
<x-wizard::progress-bar
:steps="$steps"
:currentStep="$currentStep"
/>
The component automatically calculates completion percentage based on current step position.
Form Wrapper Component
Wraps your form with CSRF protection and error handling:
<x-wizard::form-wrapper :action="route('wizard.onboarding.store', $step->id)">
<!-- Your form fields -->
<input type="text" name="name" value="" />
<input type="email" name="email" value="" />
<!-- Navigation buttons -->
</x-wizard::form-wrapper>
Automatically displays validation errors at the top of the form.
Step Navigation Component
Provides back/next/complete buttons:
<x-wizard::step-navigation
:canGoBack="$canGoBack"
:canGoForward="$canGoForward"
:isLastStep="$isLastStep"
:previousStep="$previousStep ?? null"
:nextStep="$nextStep ?? null"
backText="Previous"
nextText="Next"
completeText="Complete"
/>
Complete Example
<x-wizard::layout title="User Onboarding">
<x-wizard::progress-bar :steps="$steps" :currentStep="$currentStep" />
<x-wizard::form-wrapper :action="route('wizard.onboarding.store', 'personal-info')">
<h2>Personal Information</h2>
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" value="" />
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" value="" />
</div>
<x-wizard::step-navigation
:canGoBack="false"
:canGoForward="true"
:isLastStep="false"
:nextStep="$nextStep"
/>
</x-wizard::form-wrapper>
</x-wizard::layout>
Customization:
Publish components to customize styling:
php artisan vendor:publish --tag="wizard-components"
Components will be available in resources/views/vendor/wizard-package/components/.
Vue 3 Composable
For API/SPA wizards, use the useWizard() composable:
Installation
Publish assets:
php artisan vendor:publish --tag="wizard-assets"
Import in your Vue component:
import { useWizard } from '@/composables/useWizard';
export default {
setup() {
const {
state,
currentStep,
canGoBack,
canGoForward,
isLastStep,
initialize,
submitStep,
goToStep
} = useWizard('onboarding');
return {
state,
currentStep,
canGoBack,
canGoForward,
isLastStep,
initialize,
submitStep,
goToStep
};
}
};
Reactive State
interface WizardState {
currentStepIndex: number;
steps: WizardStep[];
formData: Record<string, any>;
errors: Record<string, string[]>;
loading: boolean;
completed: boolean;
wizardData: any;
}
Methods
Initialize Wizard
await initialize();
Submit Step
const result = await submitStep({
name: 'John Doe',
email: 'john@example.com'
});
if (result.success) {
console.log('Step completed!', result.nextStep);
} else {
console.error('Validation errors:', result.errors);
}
Navigate to Step
await goToStep('personal-info');
Form Helpers
// Set field value
setFieldValue('email', 'john@example.com');
// Get field error
const emailError = getFieldError('email');
// Clear all errors
clearErrors();
Complete Vue Example
<template>
<div v-if="!state.loading" class="wizard">
<div class="progress">
Step of
<progress :value="state.currentStepIndex + 1" :max="state.steps.length"></progress>
</div>
<h2></h2>
<form @submit.prevent="handleSubmit">
<div v-if="currentStep?.id === 'personal-info'">
<input v-model="formData.name" type="text" placeholder="Name" />
<span v-if="getFieldError('name')" class="error">
</span>
<input v-model="formData.email" type="email" placeholder="Email" />
<span v-if="getFieldError('email')" class="error">
</span>
</div>
<div class="navigation">
<button v-if="canGoBack" type="button" @click="goToStep(previousStep.id)">
Previous
</button>
<button type="submit" :disabled="state.loading">
</button>
</div>
</form>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useWizard } from '@/composables/useWizard';
const {
state,
currentStep,
canGoBack,
isLastStep,
initialize,
submitStep,
goToStep,
setFieldValue,
getFieldError
} = useWizard('onboarding');
const formData = ref<Record<string, any>>({});
onMounted(async () => {
await initialize();
});
const handleSubmit = async () => {
const result = await submitStep(formData.value);
if (result.success) {
formData.value = {};
}
};
</script>
Automatic Step Reordering
When you add a new step with a specific order, existing steps are automatically reordered:
# Existing steps: Step1 (order: 1), Step3 (order: 2)
php artisan wizard:make-step Onboarding NewStep --order=2
# Result:
# - Step1 (order: 1)
# - NewStep (order: 2) ← newly inserted
# - Step3 (order: 3) ← automatically incremented
The package scans the Steps/ directory and updates step order properties automatically. No manual file editing required!
Using the Facade
Initialize a Wizard
use Invelity\WizardPackage\Facades\Wizard;
// Initialize
Wizard::initialize('onboarding');
// Get current step
$step = Wizard::getCurrentStep();
// Process step
$result = Wizard::processStep('personal-info', [
'name' => 'John Doe',
'email' => 'john@example.com',
]);
// Navigate
$nextStep = Wizard::getNextStep();
$prevStep = Wizard::getPreviousStep();
// Check progress
$progress = Wizard::getProgress();
echo $progress->completionPercentage(); // 33%
// Complete wizard
Wizard::complete();
Frontend Integration
Vue 3 with useWizard()
Recommended approach for SPA:
<script setup>
import { useWizard } from '@/composables/useWizard';
const { state, currentStep, submitStep, initialize } = useWizard('onboarding');
onMounted(() => initialize());
const handleSubmit = async (formData) => {
const result = await submitStep(formData);
if (result.success) {
// Navigate to next step automatically
}
};
</script>
Manual Fetch API (Alternative)
If you prefer manual control or use React/Angular:
// Fetch wizard state
const response = await fetch('/wizard/onboarding/personal-info');
const { step, navigation, progress } = await response.json();
// Submit step data
const result = await fetch('/wizard/onboarding/personal-info', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com'
})
});
const { success, next_step, errors } = await result.json();
API Response Format
{
"success": true,
"message": "Step completed successfully",
"next_step": {
"id": "address",
"title": "Address Information",
"url": "/wizard/onboarding/address"
},
"progress": {
"completed": 1,
"total": 3,
"percentage": 33,
"is_complete": false
},
"navigation": {
"can_go_back": true,
"can_go_forward": true,
"previous_step": {
"id": "personal-info",
"title": "Personal Information"
},
"next_step": {
"id": "payment",
"title": "Payment Details"
}
}
}
Advanced: Custom Wizard Class
You can extend the base wizard for custom behavior:
<?php
namespace App\Wizards;
use Invelity\WizardPackage\Wizard;
class OnboardingWizard extends Wizard
{
public function onComplete(): void
{
// Custom logic when wizard completes
$user = auth()->user();
$user->update(['onboarding_completed' => true]);
// Send welcome email
Mail::to($user)->send(new WelcomeEmail());
}
public function onStepComplete(string $stepId): void
{
// Custom logic after each step
activity()
->causedBy(auth()->user())
->log("Completed step: {$stepId}");
}
}