#28 FilamentPHP応用

リアルタイムテーブル更新——Livewireのポーリングとテーブル

テーブルのリアルタイム更新が必要な場面

管理画面でリアルタイム更新が役立つ代表的なケース:

  • 注文管理(新着注文がリアルタイムで表示される)
  • タスク管理(他のユーザーの更新が即座に反映される)
  • 監視ダッシュボード(サーバーステータスの自動更新)
  • チャットサポート(新着チケットのリアルタイム表示)

Livewireポーリングによる定期更新

最もシンプルな方法はLivewireのポーリング機能です。

// app/Filament/Resources/OrderResource/Pages/ListOrders.php

namespace App\Filament\Resources\OrderResource\Pages;

use App\Filament\Resources\OrderResource;
use Filament\Resources\Pages\ListRecords;

class ListOrders extends ListRecords
{
    protected static string $resource = OrderResource::class;

    // コンポーネントレベルでのポーリング設定
    // Livewireのコンポーネントに wire:poll を追加
    protected ?string $polling = '10s';  // 10秒ごとに自動更新
}

ただし、$pollingプロパティはListRecordsに直接はありません。代わりに、カスタムビューまたはLivewireのポーリングを明示的に設定する方法を使います。

// カスタムビューを使う場合
// resources/views/filament/pages/list-orders-polling.blade.php

// またはLivewireのlift属性を使う

Filamentのテーブルコンポーネント自体のポーリングを設定するより実用的な方法:

// app/Filament/Resources/OrderResource/Pages/ListOrders.php

use Livewire\Attributes\On;

class ListOrders extends ListRecords
{
    protected static string $resource = OrderResource::class;

    // ブロードキャストイベントを受信してテーブルを更新
    #[On('order-created')]
    public function refreshTable(): void
    {
        // テーブルを強制的に再描画
        $this->dispatch('refresh-table');
    }

    public function getPollingInterval(): ?string
    {
        // テーブルのポーリング間隔(Filamentの設定)
        return '30s';
    }
}

テーブルのポーリング設定

public function table(Table $table): Table
{
    return $table
        ->poll('10s')  // 10秒ごとにテーブルデータを自動更新
        ->columns([...])
        ->filters([...]);
}

->poll('Xs')はテーブルのデータを指定した間隔でサーバーから再取得します。

Livewireイベントを使ったトリガー更新

ポーリングより効率的な方法として、特定のイベント発生時だけ更新する方法があります。

// app/Filament/Resources/OrderResource/Pages/ListOrders.php

use Livewire\Attributes\On;

class ListOrders extends ListRecords
{
    protected static string $resource = OrderResource::class;

    // 'order-status-changed' イベントを受信したらテーブルを更新
    #[On('order-status-changed')]
    public function handleOrderStatusChanged(): void
    {
        // テーブルコンポーネントを再描画
        $this->resetTable();
    }
}

イベントの発行側(Actionなど):

Action::make('processOrder')
    ->action(function (Order $record): void {
        $record->update(['status' => 'processing']);

        // Livewireイベントを発行して他のコンポーネントに通知
        $this->dispatch('order-status-changed', orderId: $record->id);

        Notification::make()
            ->title('注文を処理中に変更しました')
            ->success()
            ->send();
    })

Laravelブロードキャストとの連携

より本格的なリアルタイム更新にはLaravelのブロードキャストを使います。

// app/Events/OrderCreated.php

namespace App\Events;

use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;

class OrderCreated implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets;

    public function __construct(
        public readonly Order $order
    ) {}

    public function broadcastOn(): array
    {
        return [
            new Channel('orders'),
        ];
    }

    public function broadcastAs(): string
    {
        return 'order.created';
    }

    public function broadcastWith(): array
    {
        return [
            'order_id' => $this->order->id,
            'total'    => $this->order->total_amount,
        ];
    }
}
// ListOrders.php でブロードキャストを受信

use Livewire\Attributes\On;

class ListOrders extends ListRecords
{
    protected static string $resource = OrderResource::class;

    protected function getListeners(): array
    {
        return [
            // echo: で始まるリスナーはLaravel Echoのイベントを受信
            'echo:orders,order.created' => 'handleNewOrder',
        ];
    }

    public function handleNewOrder(array $data): void
    {
        $this->resetTable();

        Notification::make()
            ->title('新しい注文が入りました')
            ->body("注文ID: #{$data['order_id']}")
            ->success()
            ->persistent()
            ->send();
    }
}

手動リフレッシュボタンの追加

ポーリングを使わず手動リフレッシュボタンを提供する方法:

protected function getHeaderActions(): array
{
    return [
        Action::make('refresh')
            ->label('更新')
            ->icon('heroicon-o-arrow-path')
            ->color('gray')
            ->action(fn () => $this->resetTable()),

        CreateAction::make(),
    ];
}

ウィジェットのリアルタイム更新

ウィジェットもポーリングで自動更新できます。

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

    protected function getStats(): array
    {
        return [
            Stat::make('本日の注文数', Order::whereDate('created_at', today())->count())
                ->description('本日0時以降')
                ->color('primary'),

            Stat::make('処理待ち', Order::where('status', 'pending')->count())
                ->color('warning'),
        ];
    }
}

パフォーマンスへの配慮

ポーリングは定期的にサーバーにリクエストを送り続けます。

// ポーリングを条件付きで無効化
class ListOrders extends ListRecords
{
    // アクティブユーザーがいる時間帯のみポーリング
    public function table(Table $table): Table
    {
        $pollingInterval = $this->shouldEnablePolling() ? '30s' : null;

        return $table
            ->poll($pollingInterval)
            ->columns([...]);
    }

    private function shouldEnablePolling(): bool
    {
        $hour = now()->hour;
        // 営業時間内(9〜18時)のみポーリング
        return $hour >= 9 && $hour < 18;
    }
}

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

コツ: ポーリングはサーバー負荷を増大させます。同時接続ユーザーが多い場合は->poll('60s')のように間隔を長めに設定するか、Laravelブロードキャストによるプッシュ型に切り替えることを検討してください。

注意点: ->poll()はテーブルのデータが更新されてもユーザーが入力中のフォームやモーダルに影響を与えません。ただし、ポーリング中にページネーションや並び替えの状態がリセットされることがあるため注意が必要です。

ハマりポイント: Livewireのイベントは同一ページのLivewireコンポーネント間でのみ機能します。別ページ・別タブのコンポーネントに通知するにはブロードキャスト(WebSocket)が必要です。

まとめ

FilamentのリアルタイムテーブルはLivewireポーリング(->poll())とブロードキャストの2つのアプローチで実現できます。小規模アプリにはポーリング、大規模アプリにはブロードキャストを選択してください。Livewireの#[On]アトリビュートでイベントドリブンな更新を実装することで、不要なポーリングを減らしつつリアルタイム性を確保できます。