#14 FilamentPHP応用

カスタムページを作る——Resourceと独立したPageの作り方

カスタムページが必要な場面

FilamentのResourceは「一覧・作成・編集」という決まったパターンを提供しますが、それ以外のページが必要になる場面があります。

  • 設定画面(アプリケーション全体の設定を管理するページ)
  • ダッシュボード以外のカスタム分析ページ
  • 複数モデルにまたがる複合的な操作ページ
  • APIのテストやデバッグ用の管理ツールページ

make:filament-page でページを生成

php artisan make:filament-page Settings

生成ファイル:

app/Filament/Pages/Settings.php
resources/views/filament/pages/settings.blade.php

基本的なPageクラスの構造

namespace App\Filament\Pages;

use Filament\Pages\Page;
use Filament\Actions\Action;

class Settings extends Page
{
    protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
    protected static ?string $navigationLabel = '設定';
    protected static ?string $title = 'アプリケーション設定';
    protected static ?int $navigationSort = 99;
    protected static ?string $navigationGroup = '管理';

    protected static string $view = 'filament.pages.settings';

    // ナビゲーションバッジ(オプション)
    public static function getNavigationBadge(): ?string
    {
        return null;
    }

    // アクセス制御
    public static function canAccess(): bool
    {
        return auth()->user()?->hasRole('admin') ?? false;
    }
}

フォームを持つカスタムページ

設定ページのようにフォームを内包するページを作る場合は、HasFormsトレイトを使います。

namespace App\Filament\Pages;

use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Forms\Form;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\Section;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use App\Models\Setting;

class Settings extends Page implements HasForms
{
    use InteractsWithForms;

    protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';
    protected static ?string $navigationLabel = '設定';
    protected static string $view = 'filament.pages.settings';

    // フォームデータを保持するpublicプロパティ
    public ?array $data = [];

    public function mount(): void
    {
        // 既存の設定を読み込んでフォームに注入
        $this->form->fill([
            'site_name'          => Setting::get('site_name', 'My App'),
            'admin_email'        => Setting::get('admin_email'),
            'maintenance_mode'   => Setting::get('maintenance_mode', false),
            'posts_per_page'     => Setting::get('posts_per_page', 15),
        ]);
    }

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                Section::make('基本設定')
                    ->schema([
                        TextInput::make('site_name')
                            ->label('サイト名')
                            ->required()
                            ->maxLength(100),

                        TextInput::make('admin_email')
                            ->label('管理者メール')
                            ->email()
                            ->required(),
                    ]),

                Section::make('表示設定')
                    ->schema([
                        TextInput::make('posts_per_page')
                            ->label('1ページあたりの記事数')
                            ->numeric()
                            ->minValue(5)
                            ->maxValue(100)
                            ->default(15),
                    ]),

                Section::make('メンテナンス')
                    ->schema([
                        Toggle::make('maintenance_mode')
                            ->label('メンテナンスモード')
                            ->helperText('有効にすると管理者以外はサイトにアクセスできません'),
                    ]),
            ])
            ->statePath('data');
    }

    public function save(): void
    {
        $data = $this->form->getState();

        foreach ($data as $key => $value) {
            Setting::set($key, $value);
        }

        Notification::make()
            ->title('設定を保存しました')
            ->success()
            ->send();
    }

    protected function getFormActions(): array
    {
        return [
            Action::make('save')
                ->label('保存する')
                ->submit('save'),
        ];
    }
}

Bladeテンプレート

<x-filament-panels::page>
    <x-filament::section>
        <form wire:submit="save">
            {{ $this->form }}

            <div class="mt-6 flex justify-end gap-3">
                {{ $this->saveAction }}
            </div>
        </form>
    </x-filament::section>
</x-filament-panels::page>

ヘッダーアクションの定義

protected function getHeaderActions(): array
{
    return [
        Action::make('resetDefaults')
            ->label('デフォルトに戻す')
            ->color('gray')
            ->requiresConfirmation()
            ->action(function (): void {
                Setting::resetAll();
                $this->mount();  // フォームを再読み込み

                Notification::make()
                    ->title('デフォルト設定に戻しました')
                    ->info()
                    ->send();
            }),
    ];
}

ResourceのカスタムPage(ResourceにPage追加)

既存Resourceに独自ページを追加することもできます。

php artisan make:filament-page BulkImport --resource=PostResource
namespace App\Filament\Resources\PostResource\Pages;

use App\Filament\Resources\PostResource;
use Filament\Resources\Pages\Page;

class BulkImport extends Page
{
    protected static string $resource = PostResource::class;
    protected static ?string $navigationLabel = '一括インポート';
    protected static string $view = 'filament.resources.post-resource.pages.bulk-import';

    public static function getNavigationItems(array $urlParameters = []): array
    {
        // サイドバーには表示しない(ヘッダーアクションから遷移させる場合)
        return [];
    }
}

Resourceのページ一覧に追加:

// app/Filament/Resources/PostResource.php

public static function getPages(): array
{
    return [
        'index'       => Pages\ListPosts::route('/'),
        'create'      => Pages\CreatePost::route('/create'),
        'edit'        => Pages\EditPost::route('/{record}/edit'),
        'bulk-import' => Pages\BulkImport::route('/bulk-import'),
    ];
}

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

コツ: フォームを持つページでは->statePath('data')を使ってLivewireのプロパティ$dataにバインドします。これを忘れるとフォームの値がLivewireコンポーネントに正しく反映されません。

注意点: mount()はページの初期表示時に1回だけ呼ばれます。フォームの初期値のセットは必ずmount()内で$this->form->fill()を使って行ってください。

ハマりポイント: getFormActions()getHeaderActions()は別物です。getFormActions()はフォームの下にボタンを表示し、getHeaderActions()はページタイトルの右横に表示します。保存ボタンはフォームアクション、追加の操作はヘッダーアクションとして使い分けると直感的なUIになります。

まとめ

FilamentのカスタムPageはPageクラスを継承するだけで作成でき、HasFormsトレイトを追加することでフォームも統合できます。設定画面・ツールページ・カスタム分析画面など、ResourceのCRUDパターンに収まらないあらゆるページをFilamentの統一されたUIで構築できます。