#22 FilamentPHP応用

ウィザードフォーム——複数ステップのWizardコンポーネント詳解

Wizardが必要な場面

一度に多くの情報を入力するフォームはユーザーに負担をかけます。Wizardコンポーネントを使うと、フォームを複数のステップに分割し、段階的に入力を進めるUXが実現できます。

典型的なユースケース:

  • 会員登録フロー(基本情報→プロフィール→支払い設定)
  • 商品登録(基本情報→バリエーション→価格設定→在庫)
  • イベント申込(参加者情報→オプション選択→確認)

基本的なWizardの実装

use Filament\Forms\Components\Wizard;
use Filament\Forms\Components\Wizard\Step;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Wizard::make([
                Step::make('基本情報')
                    ->description('商品の基本的な情報を入力してください')
                    ->icon('heroicon-o-pencil-square')
                    ->schema([
                        TextInput::make('name')
                            ->label('商品名')
                            ->required()
                            ->maxLength(255),

                        Select::make('category_id')
                            ->label('カテゴリ')
                            ->relationship('category', 'name')
                            ->required(),

                        Textarea::make('description')
                            ->label('商品説明')
                            ->rows(4),
                    ]),

                Step::make('価格と在庫')
                    ->description('価格と在庫数を設定してください')
                    ->icon('heroicon-o-currency-yen')
                    ->schema([
                        TextInput::make('price')
                            ->label('販売価格')
                            ->numeric()
                            ->prefix('¥')
                            ->required()
                            ->minValue(0),

                        TextInput::make('compare_at_price')
                            ->label('比較価格(定価)')
                            ->numeric()
                            ->prefix('¥'),

                        TextInput::make('stock_quantity')
                            ->label('在庫数')
                            ->numeric()
                            ->default(0)
                            ->minValue(0),

                        Toggle::make('track_inventory')
                            ->label('在庫管理を有効化')
                            ->default(true),
                    ]),

                Step::make('画像とSEO')
                    ->description('画像のアップロードとSEO設定')
                    ->icon('heroicon-o-photo')
                    ->schema([
                        FileUpload::make('images')
                            ->label('商品画像')
                            ->image()
                            ->multiple()
                            ->maxFiles(8)
                            ->columnSpanFull(),

                        TextInput::make('meta_title')
                            ->label('SEOタイトル')
                            ->maxLength(60),

                        Textarea::make('meta_description')
                            ->label('SEOディスクリプション')
                            ->maxLength(160)
                            ->rows(2),
                    ]),
            ])
            ->columnSpanFull()
            ->skippable(false),  // ステップを順番通りに進める
        ]);
}

ステップごとのバリデーション

各ステップで「次へ」ボタンを押すと、そのステップのフィールドのみバリデーションが実行されます。次のステップに進むためにはすべてのバリデーションを通過する必要があります。

Step::make('会員情報')
    ->schema([
        TextInput::make('email')
            ->label('メールアドレス')
            ->email()
            ->required()
            ->unique('users', 'email')   // メール重複チェックも可能
            ->maxLength(255),

        TextInput::make('password')
            ->label('パスワード')
            ->password()
            ->required()
            ->minLength(8)
            ->same('password_confirmation'),

        TextInput::make('password_confirmation')
            ->label('パスワード(確認)')
            ->password()
            ->required()
            ->dehydrated(false),  // DBに保存しない
    ]),

->afterValidation() でステップ完了時の処理

ステップのバリデーション後に追加処理を実行できます。

Step::make('メールアドレス確認')
    ->schema([
        TextInput::make('email')
            ->label('メールアドレス')
            ->email()
            ->required(),

        TextInput::make('verification_code')
            ->label('確認コード')
            ->required()
            ->length(6)
            ->numeric(),
    ])
    ->afterValidation(function (array $state): void {
        // 確認コードをチェック
        $storedCode = cache()->get("verification_code_{$state['email']}");
        if ($storedCode !== $state['verification_code']) {
            throw ValidationException::withMessages([
                'verification_code' => '確認コードが正しくありません',
            ]);
        }
    }),

Wizard付きCreateページ

ResourceのCreateページをWizardに置き換えることができます。

// app/Filament/Resources/ProductResource/Pages/CreateProduct.php

namespace App\Filament\Resources\ProductResource\Pages;

use App\Filament\Resources\ProductResource;
use Filament\Resources\Pages\CreateRecord;
use Filament\Resources\Pages\CreateRecord\Concerns\HasWizard;

class CreateProduct extends CreateRecord
{
    use HasWizard;

    protected static string $resource = ProductResource::class;

    protected function getSteps(): array
    {
        return [
            Step::make('基本情報')
                ->schema([
                    TextInput::make('name')->required(),
                    Select::make('category_id')
                        ->relationship('category', 'name')
                        ->required(),
                ]),

            Step::make('価格と在庫')
                ->schema([
                    TextInput::make('price')->numeric()->required(),
                    TextInput::make('stock_quantity')->numeric()->default(0),
                ]),

            Step::make('確認')
                ->schema([
                    Placeholder::make('summary')
                        ->content(fn (Get $get) =>
                            "商品名: {$get('name')}\n価格: ¥" . number_format($get('price'))
                        ),
                ]),
        ];
    }
}

条件付きステップのスキップ

->skippable()をtrueにすると、ユーザーが任意のステップをスキップできるようになります。

Wizard::make([
    Step::make('基本情報')->schema([...]),
    Step::make('オプション情報')->schema([...]),
    Step::make('確認')->schema([...]),
])
->skippable()

コードでステップをスキップ(リンクとして定義)する場合はStep::make()->skippable()

Step::make('オプション情報')
    ->schema([...])
    // このステップはスキップ可能

Wizardの完了後処理をカスタマイズ

class CreateOrder extends CreateRecord
{
    use HasWizard;

    protected function afterCreate(): void
    {
        // ウィザード完了後に実行される処理
        $order = $this->getRecord();

        // 注文確認メール送信
        Mail::to($order->customer->email)
            ->send(new OrderConfirmationMail($order));

        // 在庫の減算
        foreach ($order->items as $item) {
            $item->product->decrement('stock_quantity', $item->quantity);
        }

        Notification::make()
            ->title('注文を受け付けました')
            ->body("注文番号: #{$order->order_number}")
            ->success()
            ->send();
    }

    protected function getCreatedNotificationTitle(): ?string
    {
        return null;  // デフォルト通知を無効化(afterCreate内で送信するため)
    }
}

コツ・注意点・ハマりポイント

コツ: ステップ数は3〜5が最適です。それ以上になる場合はTabsコンポーネントの使用や、フォームの設計を見直すことを検討してください。最後のステップは「確認ステップ」として入力内容の確認画面にすると、ユーザーが送信前に内容を確認できて誤操作を防げます。

注意点: WizardはLivewireのステート管理に依存しているため、ページをリロードするとステップ1に戻ります。重要なフォームデータはセッションや一時保存機能(draft状態のレコード)でバックアップすることを検討してください。

ハマりポイント: Wizard内のフィールドに->relationship()を使う場合、リレーション保存のタイミングに注意が必要です。Wizardの各ステップでは実際のDB保存は行われず、すべてのステップ完了時にまとめて保存されます。

まとめ

FilamentのWizardコンポーネントは複数ステップのフォームを簡単に実装できます。HasWizardトレイトを使えばResourceのCreateページをウィザード化でき、ステップごとのバリデーション・afterValidation()フック・完了後処理を組み合わせることで、複雑な入力フローを整理されたUXで提供できます。