223 lines
6.5 KiB
PHP
223 lines
6.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\PreferencesModel;
|
|
use App\Models\SettingsModel;
|
|
use CodeIgniter\HTTP\RequestInterface;
|
|
use CodeIgniter\I18n\Time;
|
|
|
|
class TimeService
|
|
{
|
|
private string $defaultUserTimezone = 'America/New_York';
|
|
private string $serverTimezone = 'UTC';
|
|
|
|
// Cached per-request user timezone
|
|
private ?string $cachedUserTz = null;
|
|
|
|
/**
|
|
* Prime detection cache from the Request (optional call, lazy by default).
|
|
*/
|
|
public function primeFromRequest(?RequestInterface $request = null): void
|
|
{
|
|
$this->cachedUserTz = $this->detectUserTimezone($request);
|
|
}
|
|
|
|
/**
|
|
* Get the resolved user timezone (detect lazily if needed).
|
|
*/
|
|
public function userTimezone(?RequestInterface $request = null): string
|
|
{
|
|
if ($this->cachedUserTz !== null) {
|
|
return $this->cachedUserTz;
|
|
}
|
|
|
|
$this->cachedUserTz = $this->detectUserTimezone($request);
|
|
return $this->cachedUserTz;
|
|
}
|
|
|
|
/**
|
|
* Core detection logic (headers > user pref > settings > school config > default).
|
|
*/
|
|
public function detectUserTimezone(?RequestInterface $request = null): string
|
|
{
|
|
$request = $request ?: service('request');
|
|
|
|
// 1) Explicit client header
|
|
try {
|
|
$tzHeader = $request->getHeaderLine('X-Timezone')
|
|
?: $request->getHeaderLine('Timezone')
|
|
?: $request->getHeaderLine('Accept-Timezone')
|
|
?: null;
|
|
|
|
if ($tzHeader && in_array($tzHeader, timezone_identifiers_list(), true)) {
|
|
return $tzHeader;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// ignore
|
|
}
|
|
|
|
// 2) User preference (if logged-in)
|
|
try {
|
|
$userId = (int) (session('user_id') ?: 0);
|
|
if ($userId > 0) {
|
|
// Do not select a specific column; some DBs may not have it yet
|
|
$pref = (new PreferencesModel())
|
|
->where('user_id', $userId)
|
|
->first();
|
|
$tz = $pref['timezone'] ?? null;
|
|
if ($tz && in_array($tz, timezone_identifiers_list(), true)) {
|
|
return $tz;
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// ignore
|
|
}
|
|
|
|
// 3) Global settings timezone (if available)
|
|
try {
|
|
$settings = (new SettingsModel())->getSettings();
|
|
$tz = $settings['timezone'] ?? null;
|
|
if ($tz && in_array($tz, timezone_identifiers_list(), true)) {
|
|
return $tz;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// ignore
|
|
}
|
|
|
|
// 4) School attendance timezone (if defined)
|
|
try {
|
|
$tz = (string) (config('School')->attendance['timezone'] ?? '');
|
|
if ($tz && in_array($tz, timezone_identifiers_list(), true)) {
|
|
return $tz;
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// ignore
|
|
}
|
|
|
|
// 5) Default
|
|
return $this->defaultUserTimezone;
|
|
}
|
|
|
|
/**
|
|
* Server timezone (for storage/UTC operations).
|
|
*/
|
|
public function serverTimezone(): string
|
|
{
|
|
return $this->serverTimezone;
|
|
}
|
|
|
|
/**
|
|
* Current time in user timezone as CI Time.
|
|
*/
|
|
public function nowLocal(?string $tz = null): Time
|
|
{
|
|
$tz = $tz ?: $this->userTimezone();
|
|
return Time::now($tz);
|
|
}
|
|
|
|
/**
|
|
* Current time in UTC as CI Time.
|
|
*/
|
|
public function nowUTC(): Time
|
|
{
|
|
return Time::now($this->serverTimezone);
|
|
}
|
|
|
|
/**
|
|
* Convert any supported input to UTC string (Y-m-d H:i:s by default).
|
|
*
|
|
* @param string|Time|\DateTimeInterface|null $value
|
|
*/
|
|
public function toUTC($value, ?string $fromTz = null, string $format = 'Y-m-d H:i:s'): ?string
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
$fromTz = $fromTz ?: $this->userTimezone();
|
|
|
|
try {
|
|
if ($value instanceof Time) {
|
|
return $value->setTimezone($this->serverTimezone)->toDateTimeString();
|
|
}
|
|
if ($value instanceof \DateTimeInterface) {
|
|
return Time::createFromInstance($value)
|
|
->setTimezone($this->serverTimezone)
|
|
->format($format);
|
|
}
|
|
|
|
// assume string
|
|
return Time::parse((string)$value, $fromTz)
|
|
->setTimezone($this->serverTimezone)
|
|
->format($format);
|
|
} catch (\Throwable $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert UTC (or provided timezone) to user-local string.
|
|
*
|
|
* @param string|Time|\DateTimeInterface|null $value
|
|
*/
|
|
public function toLocal($value, ?string $sourceTz = null, ?string $targetTz = null, string $format = 'Y-m-d H:i:s'): ?string
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
$targetTz = $targetTz ?: $this->userTimezone();
|
|
if ($sourceTz === null && $this->isDateOnlyString($value)) {
|
|
// Date-only strings should not shift across timezones.
|
|
$sourceTz = $targetTz;
|
|
} else {
|
|
$sourceTz = $sourceTz ?: $this->serverTimezone;
|
|
}
|
|
|
|
try {
|
|
if ($value instanceof Time) {
|
|
return $value->setTimezone($targetTz)->format($format);
|
|
}
|
|
if ($value instanceof \DateTimeInterface) {
|
|
return Time::createFromInstance($value)
|
|
->setTimezone($targetTz)
|
|
->format($format);
|
|
}
|
|
|
|
// assume string
|
|
return Time::parse((string)$value, $sourceTz)
|
|
->setTimezone($targetTz)
|
|
->format($format);
|
|
} catch (\Throwable $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convenience formatter for user-local.
|
|
*/
|
|
public function formatLocal($value, string $format = 'Y-m-d H:i', ?string $sourceTz = null): string
|
|
{
|
|
return (string) ($this->toLocal($value, $sourceTz, null, $format) ?? '');
|
|
}
|
|
|
|
/**
|
|
* Convenience formatter for UTC.
|
|
*/
|
|
public function formatUTC($value, string $format = 'Y-m-d H:i:s', ?string $fromTz = null): string
|
|
{
|
|
return (string) ($this->toUTC($value, $fromTz, $format) ?? '');
|
|
}
|
|
|
|
private function isDateOnlyString($value): bool
|
|
{
|
|
if (!is_string($value)) {
|
|
return false;
|
|
}
|
|
|
|
$value = trim($value);
|
|
return (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $value);
|
|
}
|
|
}
|