#13 FilamentPHP応用

カスタムウィジェットを作る——ダッシュボードへの独自チャート・統計の追加

Filamentのウィジェットシステム

ウィジェットはダッシュボードやResourceページのヘッダーに表示できる再利用可能なUIブロックです。Filamentは以下の種類を標準提供しています。

ウィジェット種類用途
StatsOverviewWidgetKPIカード(数値・増減・チャート付き)
ChartWidget折れ線・棒・円グラフ
TableWidgetテーブル形式
カスタム(Blade)完全自由なレイアウト

StatsOverviewWidgetの作成

php artisan make:filament-widget PostStatsOverview --stats-overview
namespace App\Filament\Widgets;

use App\Models\Post;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Support\Carbon;

class PostStatsOverview extends BaseWidget
{
    protected static ?int $sort = 1;

    protected function getStats(): array
    {
        $publishedThisMonth = Post::where('status', 'published')
            ->whereBetween('published_at', [
                Carbon::now()->startOfMonth(),
                Carbon::now()->endOfMonth(),
            ])
            ->count();

        $publishedLastMonth = Post::where('status', 'published')
            ->whereBetween('published_at', [
                Carbon::now()->subMonth()->startOfMonth(),
                Carbon::now()->subMonth()->endOfMonth(),
            ])
            ->count();

        $growth = $publishedLastMonth > 0
            ? round((($publishedThisMonth - $publishedLastMonth) / $publishedLastMonth) * 100, 1)
            : 0;

        // 過去7日の日別公開数(スパークライン用)
        $trendData = collect(range(6, 0))->map(fn (int $daysAgo): int =>
            Post::where('status', 'published')
                ->whereDate('published_at', Carbon::now()->subDays($daysAgo))
                ->count()
        )->toArray();

        return [
            Stat::make('総記事数', number_format(Post::count()))
                ->description('全ステータス合計')
                ->icon('heroicon-o-document-text')
                ->color('primary'),

            Stat::make('今月の公開数', $publishedThisMonth)
                ->description("{$growth}% vs 先月")
                ->descriptionIcon($growth >= 0 ? 'heroicon-m-arrow-trending-up' : 'heroicon-m-arrow-trending-down')
                ->color($growth >= 0 ? 'success' : 'danger')
                ->chart($trendData),

            Stat::make('下書き中', Post::where('status', 'draft')->count())
                ->description('公開待ちの記事')
                ->icon('heroicon-o-pencil-square')
                ->color('warning'),
        ];
    }
}

ChartWidgetの作成

php artisan make:filament-widget PostPublicationChart --chart
namespace App\Filament\Widgets;

use App\Models\Post;
use Filament\Widgets\ChartWidget;
use Illuminate\Support\Carbon;

class PostPublicationChart extends ChartWidget
{
    protected static ?string $heading = '月別公開数';
    protected static ?int $sort = 2;
    protected static string $color = 'info';

    // フィルタの選択肢
    protected function getFilters(): ?array
    {
        return [
            '6months' => '過去6ヶ月',
            '12months' => '過去12ヶ月',
            'this_year' => '今年',
        ];
    }

    protected function getData(): array
    {
        $filter = $this->filter ?? '6months';

        $months = match($filter) {
            '12months' => 12,
            'this_year' => Carbon::now()->month,
            default => 6,
        };

        $labels = [];
        $data = [];

        for ($i = $months - 1; $i >= 0; $i--) {
            $date = Carbon::now()->subMonths($i);
            $labels[] = $date->format('Y/m');
            $data[] = Post::where('status', 'published')
                ->whereYear('published_at', $date->year)
                ->whereMonth('published_at', $date->month)
                ->count();
        }

        return [
            'datasets' => [
                [
                    'label' => '公開数',
                    'data' => $data,
                    'fill' => 'start',
                ],
            ],
            'labels' => $labels,
        ];
    }

    protected function getType(): string
    {
        return 'line';  // 'bar', 'pie', 'doughnut' なども指定可能
    }
}

カスタムBladeウィジェット

完全に自由なレイアウトが必要な場合は、カスタムBladeウィジェットを作ります。

php artisan make:filament-widget RecentActivityFeed
namespace App\Filament\Widgets;

use Filament\Widgets\Widget;
use App\Models\Post;

class RecentActivityFeed extends Widget
{
    protected static string $view = 'filament.widgets.recent-activity-feed';
    protected static ?int $sort = 3;

    // ウィジェットを何列分占有するか(デフォルト1)
    protected int | string $columnSpan = 'full';

    // Bladeに渡すデータ
    protected function getViewData(): array
    {
        return [
            'recentPosts' => Post::with('author')
                ->latest('published_at')
                ->limit(5)
                ->get(),
        ];
    }
}

Bladeテンプレート(resources/views/filament/widgets/recent-activity-feed.blade.php):

<x-filament-widgets::widget>
    <x-filament::section>
        <x-slot name="heading">最近の公開記事</x-slot>

        <div class="divide-y divide-gray-100 dark:divide-gray-800">
            @forelse($recentPosts as $post)
                <div class="flex items-center gap-3 py-3">
                    <div class="flex-1 min-w-0">
                        <p class="text-sm font-medium text-gray-900 dark:text-white truncate">
                            {{ $post->title }}
                        </p>
                        <p class="text-xs text-gray-500 dark:text-gray-400">
                            {{ $post->author->name }} ·
                            {{ $post->published_at?->diffForHumans() }}
                        </p>
                    </div>
                    <x-filament::badge color="success">公開中</x-filament::badge>
                </div>
            @empty
                <p class="py-4 text-center text-sm text-gray-500">記事がありません</p>
            @endforelse
        </div>
    </x-filament::section>
</x-filament-widgets::widget>

ウィジェットをパネルに登録

// app/Providers/Filament/AdminPanelProvider.php

->widgets([
    PostStatsOverview::class,
    PostPublicationChart::class,
    RecentActivityFeed::class,
])

ウィジェットをResourceに紐付け

特定のResourceページにもウィジェットを表示できます。

// app/Filament/Resources/PostResource/Pages/ListPosts.php

protected function getHeaderWidgets(): array
{
    return [
        PostStatsOverview::class,
    ];
}

表示制御とポーリング

class PostStatsOverview extends BaseWidget
{
    // 30秒ごとに自動更新
    protected static ?string $pollingInterval = '30s';

    // 遅延読み込み(ページ表示後に非同期で読み込む)
    protected static bool $isLazy = true;

    // 権限チェック(trueを返すユーザーにのみ表示)
    public static function canView(): bool
    {
        return auth()->user()?->hasRole('admin') ?? false;
    }
}

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

コツ: ウィジェットはページ内で並列にレンダリングされます。$isLazy = trueにすると個別に非同期ロードされるため、ダッシュボード全体の初期表示が速くなります。重いクエリを持つウィジェットには必ず設定しましょう。

注意点: getStats()getData()内でN+1クエリが起きやすいので、withCount()with()を使ったEager Loadingを徹底してください。また、同じ数値を複数のウィジェットで参照する場合はキャッシュを利用します。

ハマりポイント: ChartWidgetのカラー指定はprotected static string $colorを使いますが、テーマカスタマイズで色が変わる可能性があります。必要に応じてgetData()内でRGB値を直接指定することもできます。

まとめ

FilamentのウィジェットはStatsOverview・Chart・カスタムBladeの3種類を使い分けることで、KPIダッシュボードから自由レイアウトの情報パネルまで実現できます。$isLazy$pollingIntervalcanView()を組み合わせることで、パフォーマンスと権限管理も適切に制御できます。