リソースのAPI連携——外部APIデータをFilamentリソースとして表示
外部APIデータをFilamentで管理する
すべてのデータがローカルDBに存在するわけではありません。外部API(Stripe・GitHub・Shopify・社内APIなど)から取得したデータをFilamentの管理画面で扱いたいケースがあります。
このエピソードでは、Eloquentモデルなしで外部APIデータをFilamentリソースとして表示・操作する方法を解説します。
アプローチ1:APIデータをEloquentモデルにキャッシュ
最もシンプルな方法は外部APIのデータを定期的にDBに同期し、Eloquentモデルを通して扱うことです。
// app/Models/StripeCustomer.php
class StripeCustomer extends Model
{
protected $fillable = [
'stripe_id', 'name', 'email', 'currency',
'balance', 'created_at_stripe',
];
protected $casts = [
'balance' => 'integer',
'created_at_stripe' => 'datetime',
];
}
// app/Console/Commands/SyncStripeCustomers.php
class SyncStripeCustomers extends Command
{
protected $signature = 'stripe:sync-customers';
public function handle(): void
{
$stripe = new \Stripe\StripeClient(config('services.stripe.secret'));
$customers = $stripe->customers->all(['limit' => 100]);
foreach ($customers->data as $customer) {
StripeCustomer::updateOrCreate(
['stripe_id' => $customer->id],
[
'name' => $customer->name,
'email' => $customer->email,
'currency' => $customer->currency,
'balance' => $customer->balance,
'created_at_stripe' => Carbon::createFromTimestamp($customer->created),
]
);
}
$this->info('Stripe顧客データを同期しました。');
}
}
このアプローチはシンプルで検索・ソート・フィルタがすべてEloquentで動作するメリットがあります。
アプローチ2:APIデータをリアルタイムに表示
DBキャッシュを使わず、APIデータを直接取得して表示するカスタムページを作る方法です。
// app/Filament/Pages/StripeCustomers.php
namespace App\Filament\Pages;
use Filament\Pages\Page;
use Illuminate\Support\Facades\Http;
class StripeCustomers extends Page
{
protected static string $view = 'filament.pages.stripe-customers';
protected static ?string $navigationIcon = 'heroicon-o-credit-card';
protected static ?string $navigationLabel = 'Stripe顧客';
public function getCustomers(): array
{
return cache()->remember('stripe_customers', 300, function (): array {
$response = Http::withToken(config('services.stripe.secret'))
->get('https://api.stripe.com/v1/customers', [
'limit' => 100,
]);
if ($response->failed()) {
return [];
}
return $response->json('data', []);
});
}
protected function getViewData(): array
{
return [
'customers' => $this->getCustomers(),
];
}
}
アプローチ3:仮想Eloquentモデルでフル機能を活かす
EloquentのAPI(ページネーション・ソート・フィルタ)をAPIデータにも使いたい場合、カスタムEloquentモデルとリポジトリパターンを組み合わせます。
// app/Models/GitHubRepository.php
class GitHubRepository extends Model
{
// DBテーブルは実際には不使用(テーブル名を存在しない名前に)
protected $table = 'github_repositories_virtual';
protected $fillable = [
'id', 'full_name', 'description', 'language',
'stargazers_count', 'forks_count', 'open_issues_count',
'html_url', 'updated_at',
];
}
ただし、このアプローチはEloquentのDBクエリが走るため実際には使いにくく、カスタムページのほうが実用的です。
カスタムTableコンポーネントを使ったAPIデータ表示
最も現実的なのは、FilamentのTableを直接使いつつデータをAPIから取得するパターンです。
namespace App\Filament\Pages;
use Filament\Pages\Page;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Filament\Tables\Columns\TextColumn;
use Illuminate\Support\Collection;
class GitHubIssues extends Page implements HasTable
{
use InteractsWithTable;
protected static string $view = 'filament.pages.github-issues';
protected static ?string $navigationIcon = 'heroicon-o-bug-ant';
protected static ?string $navigationLabel = 'GitHubイシュー';
public function table(Table $table): Table
{
return $table
->query(fn () => $this->getIssuesQuery())
->columns([
TextColumn::make('number')
->label('#')
->sortable(),
TextColumn::make('title')
->label('タイトル')
->searchable()
->limit(60)
->url(fn ($record) => $record->html_url),
TextColumn::make('user.login')
->label('作成者'),
TextColumn::make('state')
->label('状態')
->badge()
->color(fn (string $state): string =>
$state === 'open' ? 'success' : 'gray'
),
TextColumn::make('created_at')
->label('作成日')
->since(),
])
->filters([
\Filament\Tables\Filters\SelectFilter::make('state')
->options(['open' => 'Open', 'closed' => 'Closed']),
]);
}
private function getIssuesQuery(): Builder
{
// APIから取得したデータをコレクションに変換
// 実際にはAPIデータをDBにキャッシュしたモデルを使う
return GitHubIssueCache::query();
}
}
HTTPクライアントのレート制限対応
外部APIには多くの場合レート制限があります。
class GitHubApiService
{
private Http $client;
public function __construct()
{
$this->client = Http::withToken(config('services.github.token'))
->baseUrl('https://api.github.com')
->acceptJson()
->retry(3, 100, fn (\Exception $exception): bool =>
$exception instanceof \Illuminate\Http\Client\ConnectionException
);
}
public function getIssues(string $repo, array $params = []): array
{
$cacheKey = "github_issues_{$repo}_" . md5(serialize($params));
return cache()->remember($cacheKey, 600, function () use ($repo, $params): array {
$response = $this->client->get("/repos/{$repo}/issues", $params);
if ($response->tooManyRequests()) {
// レート制限に引っかかった場合はリトライ待機
$retryAfter = $response->header('Retry-After', 60);
throw new \RuntimeException("GitHub API rate limit exceeded. Retry after {$retryAfter}s");
}
return $response->json() ?? [];
});
}
}
アクションでAPI操作を実行
APIの書き込み操作もFilamentのActionとして実装できます。
Action::make('closeIssue')
->label('イシューをクローズ')
->icon('heroicon-o-x-circle')
->color('danger')
->requiresConfirmation()
->action(function (array $record): void {
$response = Http::withToken(config('services.github.token'))
->patch("https://api.github.com/repos/{$record['repo']}/issues/{$record['number']}", [
'state' => 'closed',
]);
if ($response->failed()) {
Notification::make()
->title('クローズに失敗しました')
->danger()
->send();
return;
}
cache()->forget("github_issues_{$record['repo']}*");
Notification::make()
->title('イシューをクローズしました')
->success()
->send();
})
コツ・注意点・ハマりポイント
コツ: 外部APIデータをFilamentで管理する場合、「DBキャッシュ同期方式」が最も機能的です。Eloquentのフル機能(検索・ソート・フィルタ・エクスポート)がそのまま使えます。Cronで定期同期し、webhookで差分更新する構成が理想的です。
注意点: APIデータをリアルタイム表示する場合、テーブルのページネーションやソートがデフォルトではAPIに対応しません。カスタムページとInteractsWithTableトレイトを使う場合でも、クエリはEloquentベースである必要があります。
ハマりポイント: HTTPクライアントでAPIキーを->withToken()や->withBasicAuth()で設定するのを忘れて、認証エラーが出てもエラーメッセージが分かりにくいことがあります。$response->status()と$response->json()でデバッグしてください。
まとめ
外部APIデータをFilamentで扱うには「DBキャッシュ同期」が最も実用的なアプローチです。Cronでの定期同期とwebhookによる差分更新を組み合わせることで、EloquentのフルパワーをAPIデータにも適用できます。リアルタイム性が必要なケースではInteractsWithTableを使ったカスタムページが有力な選択肢です。