リピーターとネストされたフォーム——Repeater/Nested Fieldsの設計
Repeaterコンポーネントとは
Repeaterは同じフォームフィールドのセットを動的に追加・削除・並び替えできるコンポーネントです。ECサイトの注文明細・FAQのQ&A・商品のバリエーションなど、「同じ構造のデータが複数ある」場面で使います。
基本的なRepeaterの実装
use Filament\Forms\Components\Repeater;
Repeater::make('social_links')
->label('SNSリンク')
->schema([
Select::make('platform')
->label('プラットフォーム')
->options([
'twitter' => 'X (Twitter)',
'instagram' => 'Instagram',
'facebook' => 'Facebook',
'linkedin' => 'LinkedIn',
'github' => 'GitHub',
])
->required(),
TextInput::make('url')
->label('URL')
->url()
->required(),
Toggle::make('is_primary')
->label('メインアカウント'),
])
->columns(3)
->defaultItems(1) // 初期アイテム数
->minItems(0)
->maxItems(5)
->addActionLabel('SNSリンクを追加')
->reorderable() // ドラッグで並び替え可能
->collapsible() // 折りたたみ可能
->itemLabel(fn (array $state): ?string =>
$state['platform'] ? "[{$state['platform']}] {$state['url']}" : null
)
Eloquentリレーションとの連携
Repeaterをリレーション(HasMany)に直接紐付けることができます。
// 注文と注文明細のリレーション
// Order hasMany OrderItem
Repeater::make('orderItems')
->label('注文明細')
->relationship('orderItems') // HasManyリレーション名を指定
->schema([
Select::make('product_id')
->label('商品')
->options(Product::pluck('name', 'id'))
->live()
->afterStateUpdated(function (Get $get, Set $set): void {
$product = Product::find($get('product_id'));
if ($product) {
$set('unit_price', $product->price);
$set('name', $product->name);
}
})
->required(),
TextInput::make('name')
->label('商品名')
->required(),
TextInput::make('unit_price')
->label('単価')
->numeric()
->prefix('¥')
->required(),
TextInput::make('quantity')
->label('数量')
->numeric()
->default(1)
->minValue(1)
->required(),
])
->columns(4)
Repeaterに->relationship()を指定すると、フォーム保存時に自動的にリレーション先のレコードを作成・更新・削除してくれます。
折りたたみとラベル表示
複数アイテムがある場合にUIを整理するための設定です。
Repeater::make('faq_items')
->label('FAQ')
->relationship('faqItems')
->schema([
TextInput::make('question')
->label('質問')
->required(),
RichEditor::make('answer')
->label('回答')
->required()
->toolbarButtons(['bold', 'italic', 'link', 'bulletList']),
])
->collapsible()
->collapsed(false) // デフォルトで展開
->itemLabel(fn (array $state): ?string =>
empty($state['question']) ? null : Str::limit($state['question'], 50)
)
->addActionLabel('Q&Aを追加')
->reorderableWithDragAndDrop()
->orderColumn('sort_order') // 並び順カラム
ネストされたRepeater
Repeaterの中にRepeaterを入れることもできます(商品のオプショングループとその値など)。
Repeater::make('option_groups')
->label('オプショングループ')
->schema([
TextInput::make('name')
->label('グループ名(例: サイズ・カラー)')
->required(),
Repeater::make('options')
->label('オプション値')
->schema([
TextInput::make('value')
->label('値(例: S, M, L)')
->required(),
TextInput::make('price_adjustment')
->label('価格調整')
->numeric()
->prefix('¥')
->default(0),
])
->columns(2)
->minItems(1)
->addActionLabel('値を追加'),
])
->addActionLabel('オプショングループを追加')
->maxItems(3)
並び替え順の保存
Repeaterのアイテムを並び替え可能にする場合、順序をDBに保存する必要があります。
// マイグレーション
Schema::table('faq_items', function (Blueprint $table) {
$table->integer('sort_order')->default(0);
});
// Repeaterの設定
Repeater::make('faqItems')
->relationship('faqItems')
->schema([...])
->reorderable()
->orderColumn('sort_order')
// モデルのスコープ
class FaqItem extends Model
{
protected static function booted(): void
{
static::creating(function (FaqItem $item): void {
$item->sort_order = FaqItem::where('post_id', $item->post_id)->max('sort_order') + 1;
});
}
}
Grid・Section内でのRepeater使用
RepeaterはcolumnSpanFull()でグリッドレイアウトをまたいで全幅表示にするのが一般的です。
Section::make('注文明細')
->schema([
Repeater::make('orderItems')
->relationship('orderItems')
->schema([...])
->columnSpanFull(),
Placeholder::make('total')
->label('合計金額')
->content(fn (Get $get): string => '¥' . number_format(
collect($get('orderItems') ?? [])->sum(fn ($item) =>
($item['unit_price'] ?? 0) * ($item['quantity'] ?? 0)
)
)),
])
コツ・注意点・ハマりポイント
コツ: ->itemLabel()にクロージャを渡すと各アイテムのヘッダーに動的なラベルを表示できます。Repeaterを折りたたんだ状態でもどのアイテムか分かるようになるため、UXが大きく向上します。
注意点: Repeaterと->relationship()を使う場合、リレーション先モデルの$fillableが正しく設定されていないとエラーになります。Repeaterのフィールド名とモデルの$fillableの名前を一致させてください。
ハマりポイント: ネストされたRepeater内での$get()・$set()は、デフォルトで最も近い親のRepeaterアイテムを基準とした相対パスになります。外側のRepeaterの値を参照したい場合は'../../outer_field'のような相対パスを使います。また、ネストが深くなるとパフォーマンスに影響するため、3階層以上のネストは避けることを推奨します。
まとめ
Repeaterコンポーネントは->relationship()と組み合わせることでHasManyリレーションの管理を完全に自動化できます。->orderColumn()による並び替え・折りたたみ・ネストを活用することで、複雑なデータ構造を持つフォームも整理されたUIで実装できます。