|null */ private ?array $identifiers = null; public function __construct( private readonly Settings $settings, ) {} /** * Every zone this PHP build knows, including UTC itself. * * Deliberately not filtered down to the ten continent groups the way * v1's picker was: v1 could not offer `UTC` in its own dropdown, so an * install seeded with it had a value it was unable to re-select. The * grouping that list was reaching for belongs to the picker, and lives * in grouped() below. * * @return list */ public function all(): array { return $this->identifiers ??= DateTimeZone::listIdentifiers(); } public function isValid(string $timezone): bool { return in_array($timezone, $this->all(), true); } /** * The zone to fall back on when the viewer has expressed no usable * preference of their own — and the only zone anonymous visitors ever * see. */ public function default(): string { $configured = $this->settings->get(Setting::Timezone); // Empty means the operator has never chosen one here, so the // environment still has the say it always had. if (! is_string($configured) || $configured === '') { $configured = (string) config('app.timezone'); } return $this->isValid($configured) ? $configured : 'UTC'; } public function resolve(?User $user): string { $preference = $user?->timezone; return is_string($preference) && $this->isValid($preference) ? $preference : $this->default(); } /** * The identifier list shaped for the picker, sorted, each carrying * the offset it is on *today*. * * The label keeps the region in it ("Europe / Madrid") rather than * splitting the list into region groups. The picker searches labels, * so this is what lets someone type either "Europe" or "Madrid" and * get there; grouping would have hidden the region from the one box * they are typing into. * * The offset is computed rather than stored because it is not a * property of the zone — half the world moves twice a year, so * "Europe/Madrid is UTC+01:00" is true in January and wrong in July. * Recomputing per request keeps the hint honest and costs nothing * next to the page it is rendered on. * * @return list */ public function options(): array { $now = new \DateTimeImmutable('now'); return array_map(fn (string $identifier): array => [ 'value' => $identifier, 'label' => str_replace(['/', '_'], [' / ', ' '], $identifier), 'offset' => $this->offsetLabel($identifier, $now), ], $this->all()); } /** * "UTC-03:00" — a plain ASCII hyphen, not a minus sign, so the string * survives a copy-paste into a search box. */ private function offsetLabel(string $identifier, \DateTimeImmutable $at): string { $seconds = (new DateTimeZone($identifier))->getOffset($at); $sign = $seconds < 0 ? '-' : '+'; $seconds = abs($seconds); return sprintf('UTC%s%02d:%02d', $sign, intdiv($seconds, 3600), intdiv($seconds % 3600, 60)); } }