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 todateTimeTooltip(), 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.