カスタムインフォリストエントリを作る——AbstractEntryで詳細ページをリッチに
InfoListとEntryとは
FilamentのInfoListはレコードの詳細情報を読み取り専用で表示するコンポーネントです。フォームが「入力」のためのコンポーネント群であるのに対し、InfoListは「表示」に特化しています。
標準のEntryにはTextEntry・ImageEntry・BadgeEntryなどがありますが、カスタムEntryを作ることでより表現力豊かな詳細ページが作れます。
Entryクラスの作成
php artisan make:filament-infolist-entry MapEntry
生成ファイル:
app/Infolists/Components/MapEntry.php
resources/views/infolists/components/map-entry.blade.php
Entryクラスの実装
namespace App\Infolists\Components;
use Filament\Infolists\Components\Entry;
class MapEntry extends Entry
{
protected string $view = 'infolists.components.map-entry';
// 地図のズームレベル
protected int $zoom = 14;
// 地図の高さ
protected string $height = '300px';
// マーカーラベル
protected ?string $markerLabel = null;
public function zoom(int $zoom): static
{
$this->zoom = $zoom;
return $this;
}
public function height(string $height): static
{
$this->height = $height;
return $this;
}
public function markerLabel(string $label): static
{
$this->markerLabel = $label;
return $this;
}
public function getZoom(): int
{
return $this->zoom;
}
public function getHeight(): string
{
return $this->height;
}
public function getMarkerLabel(): ?string
{
return $this->markerLabel;
}
}
Bladeテンプレートの実装
座標(緯度/経度)を地図で表示する例です。ここではLeaflet.jsを使います。
@php
$state = $getState(); // ['lat' => 35.6762, 'lng' => 139.6503] のような配列を想定
$lat = is_array($state) ? ($state['lat'] ?? null) : null;
$lng = is_array($state) ? ($state['lng'] ?? null) : null;
$zoom = $getZoom();
$height = $getHeight();
$label = $getMarkerLabel() ?? $getRecord()?->name ?? '';
@endphp
<x-dynamic-component
:component="$getEntryWrapperView()"
:entry="$entry"
>
@if($lat && $lng)
<div
id="map-{{ $getId() }}"
style="height: {{ $height }}; width: 100%; border-radius: 0.5rem;"
></div>
@push('scripts')
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var map = L.map('map-{{ $getId() }}').setView(
[{{ $lat }}, {{ $lng }}], {{ $zoom }}
);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
L.marker([{{ $lat }}, {{ $lng }}])
.addTo(map)
.bindPopup(@js($label))
.openPopup();
});
</script>
@endpush
@else
<p class="text-sm text-gray-500 dark:text-gray-400">座標データがありません</p>
@endif
</x-dynamic-component>
使用例
use App\Infolists\Components\MapEntry;
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Section::make('基本情報')
->schema([
TextEntry::make('name')
->label('店舗名'),
TextEntry::make('phone')
->label('電話番号'),
TextEntry::make('address')
->label('住所'),
])
->columns(2),
Section::make('地図')
->schema([
MapEntry::make('coordinates')
->label('所在地')
->zoom(15)
->height('400px')
->markerLabel(fn ($record) => $record->name)
->columnSpanFull(),
]),
Section::make('営業時間')
->schema([
RepeatableEntry::make('business_hours')
->label('')
->schema([
TextEntry::make('day')->label('曜日'),
TextEntry::make('open')->label('開始'),
TextEntry::make('close')->label('終了'),
])
->columns(3),
]),
]);
}
RepeatableEntryのカスタム
複数の値をリスト表示するRepeatableEntryと組み合わせると、ネストした情報も美しく表示できます。
use Filament\Infolists\Components\RepeatableEntry;
RepeatableEntry::make('order_items')
->label('注文内容')
->schema([
TextEntry::make('product_name')
->label('商品名'),
TextEntry::make('quantity')
->label('数量')
->suffix('個'),
TextEntry::make('unit_price')
->label('単価')
->money('JPY'),
TextEntry::make('subtotal')
->label('小計')
->state(fn ($record) => $record->quantity * $record->unit_price)
->money('JPY'),
])
->columns(4)
カラーチップEntryの例
別の実用例として、カラーコードを色見本付きで表示するEntryです。
namespace App\Infolists\Components;
use Filament\Infolists\Components\Entry;
class ColorEntry extends Entry
{
protected string $view = 'infolists.components.color-entry';
}
@php
$color = $getState();
@endphp
<x-dynamic-component
:component="$getEntryWrapperView()"
:entry="$entry"
>
@if($color)
<div class="flex items-center gap-2">
<div
class="h-6 w-6 rounded-full border border-gray-200 shadow-sm"
style="background-color: {{ $color }}"
></div>
<code class="text-sm text-gray-700 dark:text-gray-300">{{ $color }}</code>
</div>
@else
<span class="text-sm text-gray-400">未設定</span>
@endif
</x-dynamic-component>
コツ・注意点・ハマりポイント
コツ: $getRecord()でエントリが属するモデルレコード全体を取得できます。フィールドの値だけでなくレコードの他の属性も必要な場合に活用してください。
注意点: InfoListのEntryはデフォルトで読み取り専用です。インタラクティブな要素(クリックでコピーなど)を追加する場合はAlpine.jsを使って実装しますが、データ変更の操作はActionに委ねるのがFilamentの設計思想に沿っています。
ハマりポイント: @push('scripts')で外部ライブラリを読み込む場合、Livewireの再レンダリング時に重複読み込みが発生することがあります。onceディレクティブ(@pushOnce)を使うか、Viteでバンドルする方法を検討してください。
まとめ
Entryクラスを継承してBladeテンプレートを実装することで、地図・カラーチップ・カスタムタイムラインなど、標準Entryでは表現できない豊かな詳細ページが作れます。フォームのカスタムFieldと対になるEntryを用意することで、入力と表示で一貫したUIを維持できます。