49 lines
1.5 KiB
PHP
49 lines
1.5 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
use Illuminate\Support\Carbon;
|
|
|
|
/**
|
|
* The two halves of a wall clock, kept in one place.
|
|
*
|
|
* A `datetime-local` field has no timezone: it is the numbers a person reads
|
|
* off their own clock. That makes it a round trip with two ends, and getting
|
|
* one end right is not half a fix — it is a fresh bug. Filling a field from a
|
|
* UTC timestamp shows an operator 21:00 for a window that starts at 23:00
|
|
* their time; parsing what they type as UTC stores 21:00 for the 19:00 they
|
|
* meant. Do both, or neither works.
|
|
*
|
|
* They live as a pair here so that nobody can change one end without the other
|
|
* being in front of them.
|
|
*/
|
|
final class LocalTime
|
|
{
|
|
/** The value for a datetime-local input, in the reader's own zone. */
|
|
public static function toField(?Carbon $at): string
|
|
{
|
|
return $at?->local()->format('Y-m-d\TH:i') ?? '';
|
|
}
|
|
|
|
/**
|
|
* What a person typed into such a field, as UTC for storage.
|
|
*
|
|
* Returns null for anything unusable rather than throwing: these fields
|
|
* are read on every keystroke, and half of "2026-07-2" is normal.
|
|
*/
|
|
public static function fromField(string $value): ?Carbon
|
|
{
|
|
if (trim($value) === '') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// Parsed IN the display zone — the string carries no offset, so
|
|
// without this it would be read as UTC and silently shifted.
|
|
return Carbon::parse($value, config('app.display_timezone'))->utc();
|
|
} catch (\Throwable) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|