#03 FilamentPHP応用

カスタムフォームコンポーネント——独自フィールドを作る

なぜカスタムコンポーネントが必要か

Filamentは豊富な標準フォームフィールド(TextInput・Select・DatePicker など)を提供しています。しかし実際の開発では、カラーピッカー・住所入力・独自のUIコントロールが必要になることがあります。そういった場合にカスタムフォームコンポーネントを自作します。

カスタムコンポーネントを作ることで、Filamentの標準フィールドと同じAPIで使えるようになり、チーム内での再利用性も高まります。

php artisan make:filament-form-field でスキャフォールド生成

php artisan make:filament-form-field ColorPicker

このコマンドで以下のファイルが生成されます。

app/Forms/Components/ColorPicker.php
resources/views/forms/components/color-picker.blade.php

生成されたPHPクラスの初期状態:

namespace App\Forms\Components;

use Filament\Forms\Components\Field;

class ColorPicker extends Field
{
    protected string $view = 'forms.components.color-picker';
}

Fieldを継承しているため、標準フィールドと同じように->label()->required()->disabled()が使えます。

view() メソッドとBladeテンプレート

$viewプロパティで対応するBladeテンプレートを指定します。Bladeテンプレート側では$getState()$setState()などのクロージャが自動的に利用可能です。

@php
    $state = $getState();
@endphp

<x-dynamic-component
    :component="$getFieldWrapperView()"
    :field="$field"
>
    <div
        x-data="{ color: @js($state ?? '#000000') }"
        class="flex items-center gap-3"
    >
        <input
            type="color"
            x-model="color"
            x-on:change="$wire.set('{{ $getStatePath() }}', color)"
            class="h-10 w-16 cursor-pointer rounded border border-gray-300"
        />
        <input
            type="text"
            x-model="color"
            x-on:change="$wire.set('{{ $getStatePath() }}', color)"
            class="block w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
            placeholder="#000000"
        />
    </div>
</x-dynamic-component>

x-dynamic-component :component="$getFieldWrapperView()" はFilamentの標準ラッパー(ラベル・エラーメッセージ表示を担う)を呼び出す決まり文句です。

$getState() / $setState() のJavaScript連携

Filamentはフォームの状態管理にLivewireを使っています。Bladeテンプレート内で使える主な関数:

関数説明
$getState()現在のフィールド値を取得
$getStatePath()Livewireのwireモデルパス(data.colorなど)
$isDisabled()無効状態かどうか
$isRequired()必須かどうか
$getId()フィールドのHTML id

JavaScript側からLivewireの状態を更新するには$wire.set('{{ $getStatePath() }}', value)を使います。

カスタムPHPメソッドの追加

カスタムフィールドにも独自のメソッドを追加してFluentインターフェースを実現できます。

class ColorPicker extends Field
{
    protected string $view = 'forms.components.color-picker';
    protected bool $showAlphaChannel = false;

    public function withAlpha(): static
    {
        $this->showAlphaChannel = true;
        return $this;
    }

    public function getShowAlphaChannel(): bool
    {
        return $this->showAlphaChannel;
    }
}

使用側:

ColorPicker::make('brand_color')
    ->label('ブランドカラー')
    ->withAlpha()
    ->required()

Bladeテンプレート側では$getShowAlphaChannel()で値を参照できます。

->postfix()->prefix() アドオン

TextInputなど標準フィールドには前後にテキストやアイコンを追加できます。カスタムフィールドにも同様の概念を持たせることができます。

TextInput::make('price')
    ->prefix('¥')
    ->postfix('円(税込)')
    ->numeric()
    ->minValue(0)

TextInput::make('domain')
    ->prefix('https://')
    ->postfix('.example.com')

これにより入力の文脈が明確になり、入力ミスを減らせます。

カスタムレイアウトコンポーネント:Fieldset・Tabs・Wizard

Filamentのフォームレイアウトはフィールドのグループ化に使うコンポーネントも豊富です。

Fieldset — フィールドのグループ化

use Filament\Forms\Components\Fieldset;

Fieldset::make('SEO設定')
    ->schema([
        TextInput::make('meta_title')
            ->label('メタタイトル')
            ->maxLength(60),
        Textarea::make('meta_description')
            ->label('メタディスクリプション')
            ->maxLength(160),
        TextInput::make('canonical_url')
            ->label('Canonical URL')
            ->url(),
    ])
    ->columns(1)

Tabs — タブ切り替えフォーム

use Filament\Forms\Components\Tabs;

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

        Tabs\Tab::make('SEO')
            ->icon('heroicon-o-magnifying-glass')
            ->schema([
                TextInput::make('meta_title'),
                Textarea::make('meta_description'),
            ]),

        Tabs\Tab::make('公開設定')
            ->icon('heroicon-o-calendar')
            ->schema([
                Select::make('status')
                    ->options(['draft' => '下書き', 'published' => '公開中']),
                DateTimePicker::make('published_at'),
            ]),
    ])

Wizard — ステップフォーム

複数ステップにわたる入力フローにはWizardが適しています。

use Filament\Forms\Components\Wizard;

Wizard::make([
    Wizard\Step::make('基本情報')
        ->description('記事の基本情報を入力してください')
        ->icon('heroicon-o-pencil')
        ->schema([
            TextInput::make('title')
                ->label('タイトル')
                ->required(),
            Select::make('category_id')
                ->label('カテゴリ')
                ->relationship('category', 'name')
                ->required(),
        ]),

    Wizard\Step::make('本文')
        ->description('記事の本文を入力してください')
        ->icon('heroicon-o-document-text')
        ->schema([
            RichEditor::make('body')
                ->required()
                ->columnSpanFull(),
            FileUpload::make('thumbnail')
                ->label('サムネイル')
                ->image(),
        ]),

    Wizard\Step::make('公開設定')
        ->description('公開日時と設定を確認してください')
        ->icon('heroicon-o-paper-airplane')
        ->schema([
            Select::make('status')
                ->options(['draft' => '下書き', 'published' => '公開'])
                ->default('draft'),
            DateTimePicker::make('published_at')
                ->label('公開日時'),
            TagsInput::make('tags')
                ->label('タグ'),
        ]),
])
->skippable()  // ステップをスキップ可能にする

->skippable()を付けると各ステップへのナビゲーションが自由になります。付けない場合は順番通りに進む必要があります。

Section コンポーネントで折りたたみ可能なセクション

use Filament\Forms\Components\Section;

Section::make('詳細設定')
    ->description('任意の詳細設定項目です')
    ->icon('heroicon-o-cog-6-tooth')
    ->collapsible()       // 折りたたみ可能
    ->collapsed()         // デフォルトで折りたたまれた状態
    ->schema([
        Toggle::make('allow_comments')->label('コメントを許可'),
        Toggle::make('featured')->label('特集記事に設定'),
        TextInput::make('sort_order')->numeric()->default(0),
    ])

Grid コンポーネントで列レイアウト

use Filament\Forms\Components\Grid;

Grid::make(3)
    ->schema([
        TextInput::make('first_name')->label('名'),
        TextInput::make('last_name')->label('姓'),
        TextInput::make('phone')->label('電話番号'),
    ])

フィールド側で->columnSpan(2)を指定することで特定フィールドを複数列にまたがらせることもできます。

まとめ

Filamentのフォームコンポーネントシステムは高い拡張性を持っています。make:filament-form-fieldでカスタムフィールドを作り、TabsWizardFieldsetなどのレイアウトコンポーネントを組み合わせることで、複雑なフォームもきれいに整理できます。次のエピソードではNotificationシステムとリアルタイム更新を解説します。