#05 FilamentPHP基礎

リレーションを持つリソース——BelongsToとHasMany

リレーションとFilament

Laravelのリレーションとfilamentは深く統合されています。BelongsTo はフォームのSelectフィールドで、HasMany はRelationManagerで管理するのが基本パターンです。

今回はブログシステムを例にします。

Category (カテゴリ)
└── Post (記事) ← BelongsTo Category
    └── Comment (コメント) ← HasMany Comments

モデルとマイグレーションの準備

まずカテゴリモデルを作成します。

php artisan make:model Category -m
<?php
// 📁 database/migrations/xxxx_create_categories_table.php

Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->timestamps();
});

Postテーブルにカテゴリの外部キーを追加します。

php artisan make:migration add_category_id_to_posts_table
<?php
// 📁 database/migrations/xxxx_add_category_id_to_posts_table.php

Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('category_id')
          ->nullable()
          ->constrained()
          ->nullOnDelete();
});

コメントモデルも作成します。

php artisan make:model Comment -m
<?php
// 📁 database/migrations/xxxx_create_comments_table.php

Schema::create('comments', function (Blueprint $table) {
    $table->id();
    $table->foreignId('post_id')->constrained()->cascadeOnDelete();
    $table->string('author_name');
    $table->string('author_email');
    $table->text('body');
    $table->boolean('is_approved')->default(false);
    $table->timestamps();
});
php artisan migrate

モデルにリレーションを定義

<?php
// 📁 app/Models/Post.php

class Post extends Model
{
    protected $fillable = ['title', 'content', 'is_published', 'category_id'];

    // Post は Category に属する
    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    // Post は複数の Comment を持つ
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }
}
<?php
// 📁 app/Models/Category.php

class Category extends Model
{
    protected $fillable = ['name', 'slug'];

    // Category は複数の Post を持つ
    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}
<?php
// 📁 app/Models/Comment.php

class Comment extends Model
{
    protected $fillable = ['post_id', 'author_name', 'author_email', 'body', 'is_approved'];

    protected $casts = ['is_approved' => 'boolean'];

    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}

BelongsTo をフォームで扱う

PostResourceのフォームにカテゴリ選択を追加します。

<?php
// 📁 app/Filament/Resources/PostResource.php

use Filament\Forms\Components\Select;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            TextInput::make('title')
                ->label('タイトル')
                ->required(),

            // BelongsTo: リレーション名と表示カラムを指定
            Select::make('category_id')
                ->label('カテゴリ')
                ->relationship(
                    name: 'category',       // リレーションメソッド名
                    titleAttribute: 'name', // 表示するカラム名
                )
                ->searchable()  // 入力で絞り込み
                ->preload()     // 選択肢を事前ロード
                ->nullable()    // null許容
                ->createOptionForm([
                    // インライン作成フォーム(+ボタンで開く)
                    TextInput::make('name')
                        ->label('カテゴリ名')
                        ->required(),
                    TextInput::make('slug')
                        ->label('スラッグ')
                        ->required(),
                ]),

            // ...
        ]);
}

->createOptionForm([...]) を追加すると、Selectの隣に「+」ボタンが表示され、その場で新しいカテゴリを作成できます。


BelongsTo をテーブルで表示する

関連モデルのカラムはドット記法でアクセスできます。

public static function table(Table $table): Table
{
    return $table
        ->columns([
            TextColumn::make('title')
                ->label('タイトル')
                ->searchable(),

            // category.name でリレーション先のカラムにアクセス
            TextColumn::make('category.name')
                ->label('カテゴリ')
                ->sortable()
                ->badge()
                ->color('primary'),

            // ...
        ]);
}

テーブルに ->searchable() を追加する場合は、eager loadingが必要です。リソースに以下を追加します。

public static function getEloquentQuery(): Builder
{
    return parent::getEloquentQuery()
        ->with(['category']); // N+1問題を防ぐ
}

RelationManager を作成する

RelationManagerは編集ページの下部に表示されるサブテーブルです。Post編集画面でそのPostに紐づくCommentを管理できます。

php artisan make:filament-relation-manager PostResource comments body

引数:

  1. PostResource → 親リソース名
  2. comments → リレーションメソッド名
  3. body → テーブルに表示するカラム名

生成されるファイル:

app/Filament/Resources/PostResource/RelationManagers/
└── CommentsRelationManager.php

CommentsRelationManager を実装する

<?php
// 📁 app/Filament/Resources/PostResource/RelationManagers/CommentsRelationManager.php

namespace App\Filament\Resources\PostResource\RelationManagers;

use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;

class CommentsRelationManager extends RelationManager
{
    // リレーションメソッド名
    protected static string $relationship = 'comments';

    // サブテーブルのラベル
    protected static ?string $title = 'コメント';

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                Forms\Components\TextInput::make('author_name')
                    ->label('投稿者名')
                    ->required(),

                Forms\Components\TextInput::make('author_email')
                    ->label('メールアドレス')
                    ->email()
                    ->required(),

                Forms\Components\Textarea::make('body')
                    ->label('コメント本文')
                    ->required()
                    ->columnSpanFull(),

                Forms\Components\Toggle::make('is_approved')
                    ->label('承認済み'),
            ]);
    }

    public function table(Table $table): Table
    {
        return $table
            ->recordTitleAttribute('body')
            ->columns([
                Tables\Columns\TextColumn::make('author_name')
                    ->label('投稿者'),

                Tables\Columns\TextColumn::make('body')
                    ->label('コメント')
                    ->limit(60),

                Tables\Columns\IconColumn::make('is_approved')
                    ->label('承認')
                    ->boolean(),

                Tables\Columns\TextColumn::make('created_at')
                    ->label('投稿日時')
                    ->dateTime('Y/m/d H:i'),
            ])
            ->filters([
                Tables\Filters\TernaryFilter::make('is_approved')
                    ->label('承認状態'),
            ])
            ->headerActions([
                Tables\Actions\CreateAction::make(),
            ])
            ->actions([
                Tables\Actions\EditAction::make(),
                Tables\Actions\DeleteAction::make(),

                // 承認ボタン
                Tables\Actions\Action::make('approve')
                    ->label('承認')
                    ->icon('heroicon-o-check')
                    ->color('success')
                    ->visible(fn ($record) => ! $record->is_approved)
                    ->action(fn ($record) => $record->update(['is_approved' => true])),
            ])
            ->bulkActions([
                Tables\Actions\BulkActionGroup::make([
                    Tables\Actions\DeleteBulkAction::make(),
                ]),
            ]);
    }
}

PostResourceに RelationManager を登録

<?php
// 📁 app/Filament/Resources/PostResource.php

public static function getRelations(): array
{
    return [
        RelationManagers\CommentsRelationManager::class,
    ];
}

これで Post 編集ページの下部にコメントのサブテーブルが表示されます。


HasManyThrough の扱い

hasManyThrough も通常の hasMany と同じように RelationManager で扱えます。

// User → Post → Comment の through リレーション
class User extends Model
{
    public function comments(): HasManyThrough
    {
        return $this->hasManyThrough(Comment::class, Post::class);
    }
}

RelationManager の $relationship にそのまま指定します。

php artisan make:filament-relation-manager UserResource comments body

Morph リレーションの基本

MorphManymorphMany を使った RelationManager で対応できます。

たとえば Post と Product の両方に Image を紐付ける場合:

// 📁 app/Models/Post.php
public function images(): MorphMany
{
    return $this->morphMany(Image::class, 'imageable');
}

RelationManager の生成はHasManyと同じです。

php artisan make:filament-relation-manager PostResource images path

RelationManager 内では $relationship = 'images' と定義するだけで動作します。


BelongsToMany(多対多)

TagとPostの多対多リレーションもRelationManagerで管理できます。

php artisan make:filament-relation-manager PostResource tags name
// 📁 CommentsRelationManager.php の代わりに TagsRelationManager.php
protected static string $relationship = 'tags';

->attachAction() / ->detachAction() を使うと、既存のTagをアタッチ・デタッチできます。

->headerActions([
    Tables\Actions\AttachAction::make()
        ->preloadRecordSelect(),  // 既存タグを選択してアタッチ
])
->actions([
    Tables\Actions\DetachAction::make(), // アタッチを解除
    Tables\Actions\EditAction::make(),
])
->bulkActions([
    Tables\Actions\BulkActionGroup::make([
        Tables\Actions\DetachBulkAction::make(),
    ]),
])

まとめ

  • BelongsTo はフォームの Select::make()->relationship() で実装
  • テーブルでは TextColumn::make('category.name') のドット記法でリレーション先を表示
  • make:filament-relation-manager で HasMany のサブテーブルを生成
  • RelationManager は getRelations() に登録することで編集ページに追加される
  • HasManyThrough / MorphMany / BelongsToMany も同様のパターンで対応可能
  • N+1問題を防ぐため getEloquentQuery()with() を設定する

次回はフォームのSection・条件付き表示・フィールド間連動を学びます。