#06 FilamentPHP基礎

バリデーションとフォームの整理——Sectionと条件付き表示

フォームのレイアウト課題

フィールドが増えてくると、フォームが縦に長くなり使いにくくなります。FilamentにはSectionGridTabsなどのレイアウトコンポーネントが用意されており、フォームを構造化できます。

また ->hidden() / ->visible() / ->live() を使うと、他のフィールドの値に応じて動的にUIを変化させられます。


Section でフィールドをグループ化

Section::make() は関連するフィールドをまとめてカードUI(折りたたみ可能)で表示します。

use Filament\Forms\Components\Section;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            // 基本情報セクション
            Section::make('基本情報')
                ->description('記事の基本的な情報を入力してください')
                ->schema([
                    TextInput::make('title')
                        ->label('タイトル')
                        ->required()
                        ->maxLength(255),

                    TextInput::make('slug')
                        ->label('スラッグ')
                        ->required()
                        ->unique(ignoreRecord: true),
                ])
                ->columns(2),

            // メディアセクション(折りたたみ可能)
            Section::make('メディア')
                ->schema([
                    FileUpload::make('thumbnail')
                        ->label('サムネイル画像')
                        ->image()
                        ->disk('public'),
                ])
                ->collapsed()    // 最初から折りたたんで表示
                ->collapsible(), // 折りたたみを許可

            // 公開設定セクション(右サイドバー風)
            Section::make('公開設定')
                ->schema([
                    Toggle::make('is_published')
                        ->label('公開する'),
                    DateTimePicker::make('published_at')
                        ->label('公開日時'),
                ])
                ->aside(), // サイドバー表示(左に説明、右にフィールド)
        ]);
}

Section のオプション

メソッド説明
->columns(2)セクション内を2カラムレイアウトに
->collapsed()初期状態で折りたたむ
->collapsible()ユーザーが折りたためるようにする
->aside()左にタイトル・説明、右にフィールドのレイアウト
->compact()パディングを小さくしてコンパクト表示
->icon('heroicon-o-cog')セクションタイトル横にアイコン

Grid で横並びレイアウト

Grid::make() はSectionを使わず、フィールドをグリッドで並べます。

use Filament\Forms\Components\Grid;

Grid::make(3) // 3カラムグリッド
    ->schema([
        TextInput::make('first_name')
            ->label('名'),
        TextInput::make('last_name')
            ->label('姓'),
        TextInput::make('phone')
            ->label('電話番号'),
    ]),

// 個別フィールドでカラム数を指定
TextInput::make('address')
    ->label('住所')
    ->columnSpan(2),  // 3カラム中2カラム分の幅

TextInput::make('zip')
    ->label('郵便番号')
    ->columnSpan(1),  // 残り1カラム

Tabs でタブ切り替えレイアウト

use Filament\Forms\Components\Tabs;

Tabs::make('設定')
    ->tabs([
        Tabs\Tab::make('基本情報')
            ->icon('heroicon-o-document')
            ->schema([
                TextInput::make('title')->required(),
                Textarea::make('content'),
            ]),

        Tabs\Tab::make('SEO設定')
            ->icon('heroicon-o-magnifying-glass')
            ->schema([
                TextInput::make('meta_title')
                    ->label('メタタイトル'),
                Textarea::make('meta_description')
                    ->label('メタディスクリプション'),
            ]),

        Tabs\Tab::make('公開設定')
            ->icon('heroicon-o-globe-alt')
            ->schema([
                Toggle::make('is_published'),
                DateTimePicker::make('published_at'),
            ]),
    ])
    ->columnSpanFull(),

条件付き表示:hidden / visible / disabled

フィールドの表示・非表示・無効化を動的に制御するには Forms\Get クロージャを使います。

use Filament\Forms\Get;
use Filament\Forms\Set;

// is_published が true のときだけ published_at を表示
DateTimePicker::make('published_at')
    ->label('公開日時')
    ->visible(fn (Get $get): bool => $get('is_published')),

// type が 'external' のとき URL フィールドを表示
TextInput::make('url')
    ->label('外部URL')
    ->url()
    ->hidden(fn (Get $get): bool => $get('type') !== 'external'),

// 新規作成時のみ入力可(編集時は無効化)
TextInput::make('username')
    ->label('ユーザー名')
    ->disabled(fn (string $operation): bool => $operation === 'edit'),

// 特定の条件でフィールドを必須にする
TextInput::make('company_name')
    ->label('会社名')
    ->required(fn (Get $get): bool => $get('is_corporate')),

live() でリアルタイム更新

->live() を付けたフィールドは値が変わるたびにLivewireのリクエストが発生し、他のフィールドの状態をリアルタイムに更新します。

Toggle::make('is_published')
    ->label('公開する')
    ->live(),   // これを付けると値変更時に他フィールドが再評価される

DateTimePicker::make('published_at')
    ->label('公開日時')
    ->visible(fn (Get $get): bool => $get('is_published')),

->live(onBlur: true) にするとフォーカスが外れたタイミングに変更します(リクエスト数を減らしたい場合)。


afterStateUpdated でフィールド間連動

->afterStateUpdated() を使うと、あるフィールドの変更に反応して別のフィールドの値をセットできます。

// タイトルの入力からスラッグを自動生成
TextInput::make('title')
    ->label('タイトル')
    ->live(onBlur: true)
    ->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
        // タイトルが変わったらスラッグを更新(スラッグが未変更の場合のみ)
        if (($get('slug') ?? '') === \Str::slug($old)) {
            $set('slug', \Str::slug($state));
        }
    }),

TextInput::make('slug')
    ->label('スラッグ')
    ->required(),

// カテゴリ変更でサブカテゴリをリセット
Select::make('category_id')
    ->label('カテゴリ')
    ->options(Category::pluck('name', 'id'))
    ->live()
    ->afterStateUpdated(fn (Set $set) => $set('subcategory_id', null)),

Select::make('subcategory_id')
    ->label('サブカテゴリ')
    ->options(fn (Get $get) =>
        Subcategory::where('category_id', $get('category_id'))
            ->pluck('name', 'id')
            ->toArray()
    ),

カスタムバリデーション rules()

Laravelのバリデーションルールをそのまま使えます。

// 文字列ルール
TextInput::make('username')
    ->rules(['required', 'alpha_dash', 'min:3', 'max:20']),

// Ruleクラスを使ったユニークチェック(編集時は自分自身を除外)
TextInput::make('email')
    ->rules([
        'required',
        'email',
        fn (string $operation, $record) => $operation === 'create'
            ? \Illuminate\Validation\Rule::unique('users', 'email')
            : \Illuminate\Validation\Rule::unique('users', 'email')->ignore($record?->id),
    ]),

// クロージャでカスタムルール
TextInput::make('password_confirm')
    ->label('パスワード(確認)')
    ->password()
    ->rules([
        fn (Get $get): \Closure => function (string $attribute, $value, \Closure $fail) use ($get) {
            if ($value !== $get('password')) {
                $fail('パスワードが一致していません。');
            }
        },
    ]),

ヘルパーテキストとヒント

フィールドにサポートテキストを追加できます。

TextInput::make('slug')
    ->label('スラッグ')
    ->helperText('URLに使用されます。英数字とハイフンのみ使用できます。')
    ->hint('例: my-first-post')
    ->hintIcon('heroicon-o-information-circle'),

TextInput::make('price')
    ->label('価格')
    ->prefix('¥')     // 入力欄の前に表示
    ->suffix('円'),   // 入力欄の後に表示

TextInput::make('api_key')
    ->label('APIキー')
    ->suffixAction(
        Forms\Components\Actions\Action::make('generate')
            ->label('生成')
            ->icon('heroicon-o-arrow-path')
            ->action(fn (Set $set) => $set('api_key', \Str::random(32)))
    ),

完成形:整理されたフォーム例

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Section::make('基本情報')
                ->schema([
                    TextInput::make('title')
                        ->label('タイトル')
                        ->required()
                        ->live(onBlur: true)
                        ->afterStateUpdated(function (Get $get, Set $set, ?string $old, ?string $state) {
                            if (($get('slug') ?? '') === \Str::slug($old)) {
                                $set('slug', \Str::slug($state));
                            }
                        }),

                    TextInput::make('slug')
                        ->label('スラッグ')
                        ->required()
                        ->unique(ignoreRecord: true)
                        ->helperText('URLに使用されます'),
                ])
                ->columns(2),

            Section::make('本文')
                ->schema([
                    RichEditor::make('content')
                        ->label('本文')
                        ->required()
                        ->columnSpanFull(),
                ]),

            Section::make('公開設定')
                ->schema([
                    Toggle::make('is_published')
                        ->label('公開する')
                        ->live(),

                    DateTimePicker::make('published_at')
                        ->label('公開日時')
                        ->visible(fn (Get $get): bool => (bool) $get('is_published'))
                        ->seconds(false),
                ])
                ->columns(2)
                ->aside(),
        ]);
}

まとめ

  • Section::make() でフィールドをグループ化、折りたたみ・サイドバーレイアウトに対応
  • Grid::make() で横並びレイアウト、->columnSpan() で幅を制御
  • Tabs::make() でタブ切り替えレイアウト
  • ->hidden() / ->visible() / ->disabled()Get クロージャを渡して動的制御
  • ->live() で値変更時にLivewireを再レンダリング
  • ->afterStateUpdated() でフィールド間の値連動を実装
  • ->rules([]) でLaravelバリデーションルールを直接指定
  • ->helperText() / ->hint() / ->prefix() / ->suffix() でUI補助テキストを追加

次回はカスタムページとウィジェットでダッシュボードを作成します。