Skip to content

0022. Subclass Filament columns for a UTC tooltip, not a macro

Date: 2026-09-11 Status: Accepted

Context

ADR-0018 made every instant Conflux stores a UTC value, and named CFX-203 — labelling the admin panel's datetimes as UTC — as the change that decision made true. The admin panel and Filament's own default timezone are both UTC, so CFX-203 is a labelling fix, not a conversion: every ->dateTime() and ->since() call across app/Filament/Resources/** needed to say, in the UI, what zone the value it renders is already in.

Around 500 TextColumn/TextEntry call sites exist in app/Filament/Resources/**, of which 14 files call ->dateTime() or ->since() and needed the label. Whatever mechanism added the tooltip had to reach those call sites without touching the other ~490 unrelated ones, and without renaming a widely-used fluent method that call sites elsewhere would keep expecting to behave the plain Filament way.

A Filament macro was the first approach: TextColumn::macro('utcDateTime', ...) registered once in AdminPanelProvider::boot(), mirroring how Laravel macros extend a class without touching its source. It failed for a reason specific to this framework rather than to macros in general. Filament's components use their own Filament\Support\Concerns\Macroable, structurally different from Laravel's Illuminate\Support\Traits\Macroable, and Larastan's macro-reflection extension does not recognise it. Every call to ->utcDateTime() therefore failed PHPStan level 10 with "undefined method", not because the code was wrong, but because the checker could not see the macro at all. Teaching PHPStan about it was not a safe option scoped to this one call: TextColumn and TextEntry are the ~500-call-site classes above, and a change to how PHPStan resolves methods on them risks breaking reflection for every one of those calls, for a single new macro.

Decision

A UTC-labelling behaviour is added by subclassing the Filament component, not by macro. App\Filament\Support\TextColumn extends Filament\Tables\Columns\TextColumn, and App\Filament\Support\TextEntry extends Filament\Infolists\Components\TextEntry. Each overrides two methods:

  • dateTime() calls the parent implementation, then appends ->tooltip('All times are shown in UTC.').
  • since() calls the parent implementation, then appends "UTC" to the format string already passed to dateTimeTooltip(), so the existing hover-for-absolute-time behaviour keeps working and states its zone.

A call site adopts the behaviour by swapping one use statement — Filament\Tables\Columns\TextColumn for App\Filament\Support\TextColumn (and the TextEntry equivalent) — with no rename at the call site itself: ->dateTime() and ->since() keep their plain Filament names and signatures.

Ordinary inheritance is something PHPStan already understands natively. App\Filament\Support\TextColumn declaring dateTime(): static is just a normal override the checker resolves like any other, so subclassing introduced zero new PHPStan errors, confirmed against the pre-existing baseline of 135 errors on master, unrelated to this change. The other ~490 call sites are entirely unaffected, because they still import the plain Filament classes; only the 14 files that needed the label changed their import.

Consequences

Adding UTC labelling to a new date column is a one-line import swap, not a new method to learn: import App\Filament\Support\TextColumn (or TextEntry) in place of the Filament base class, and call ->dateTime() or ->since() exactly as before.

A field where "UTC" has no meaning is left on the plain Filament class. CustomerView.php's date_of_birth uses the raw Filament ->date() — a calendar date, not a date-time — and was not migrated, because a birthdate carries no time-of-day for a zone to qualify.

The pattern generalises beyond this one label. Any future cross-cutting change to how a Filament column or entry renders — one that must reach a specific subset of call sites without touching the rest, and must stay visible to PHPStan — should default to a subclass in App\Filament\Support over a macro, for the same reason: Filament's Macroable is not the one Larastan's reflection extension knows how to see through.

The subclass only overrides what it needs to change. Every other fluent method (->label(), ->sortable(), ->searchable(), and the rest) is inherited unmodified from the Filament base class, so App\Filament\Support\TextColumn and App\Filament\Support\TextEntry behave exactly like their parents except where this ADR says otherwise.