307 lines
11 KiB
PHP
307 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Leantime\Core\Support;
|
|
|
|
use Carbon\Carbon;
|
|
use Carbon\CarbonImmutable;
|
|
use Carbon\Exceptions\InvalidDateException;
|
|
use Carbon\Exceptions\InvalidFormatException;
|
|
use DateTime;
|
|
use DateTimeInterface;
|
|
use DateTimeZone;
|
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
|
use Leantime\Core\Configuration\Environment;
|
|
use Leantime\Core\Language;
|
|
|
|
/**
|
|
* Class DateTimeHelper
|
|
*
|
|
* A helper class for working with dates and times.
|
|
* This class should NOT contain any formatting methods. Any datetime formatting should be included into the
|
|
* CarbonMacros class
|
|
*
|
|
* @mixin CarbonMacros
|
|
*/
|
|
class DateTimeHelper extends CarbonImmutable
|
|
{
|
|
private string $userTimezone;
|
|
|
|
private string $userLanguage;
|
|
|
|
private string $userDateFormat;
|
|
|
|
private string $userTimeFormat;
|
|
|
|
private readonly string $dbTimezone;
|
|
|
|
private readonly string $dbFormat;
|
|
|
|
private ?CarbonImmutable $datetime;
|
|
|
|
/**
|
|
* Constructs a new instance of the class.
|
|
*
|
|
* @param DateTimeInterface|null|string $time Optional. The datetime object, ISO format string, or null.
|
|
* @param DateTimeZone|null|string $tz Optional. The timezone object, timezone identifier, or null.
|
|
*
|
|
* @throws BindingResolutionException
|
|
*/
|
|
public function __construct($time = null, $tz = null)
|
|
{
|
|
parent::__construct($time, $tz);
|
|
|
|
// Check if our custom macros are already registered
|
|
if (! static::hasMacro('formatDateTimeForDb')) {
|
|
static::mixin(new CarbonMacros(
|
|
session('usersettings.timezone') ?? app()->make(Environment::class)->defaultTimezone,
|
|
str_replace('-', '_', session('usersettings.language') ?? app()->make(Environment::class)->language),
|
|
session('usersettings.date_format') ?? app()->make(Language::class)->__('language.dateformat'),
|
|
session('usersettings.time_format') ?? app()->make(Language::class)->__('language.timeformat')
|
|
));
|
|
}
|
|
|
|
// Continue with regular initialization
|
|
$language = app()->make(Language::class);
|
|
$config = app()->make(Environment::class);
|
|
|
|
// These are read only for a reason
|
|
$this->dbFormat = 'Y-m-d H:i:s';
|
|
$this->dbTimezone = 'UTC';
|
|
|
|
// Session is set in middleware, unlikely to not be set but just in case set defaults.
|
|
$this->userTimezone = session('usersettings.timezone') ?? $config->defaultTimezone;
|
|
$this->userLanguage = str_replace('-', '_', (session('usersettings.language') ?? $config->language));
|
|
|
|
$this->userDateFormat = session('usersettings.date_format') ?? $language->__('language.dateformat');
|
|
$this->userTimeFormat = session('usersettings.time_format') ?? $language->__('language.timeformat');
|
|
}
|
|
|
|
/**
|
|
* Parses a user input date and time and returns a CarbonImmutable object.
|
|
*
|
|
* @param string $userDate The user input date in the format specified by $this->userDateFormat.
|
|
* @param ?string $userTime The user input time in the format specified by $this->userTimeFormat.
|
|
* Defaults to an empty string. Can also be one of start|end to denote start or end time of
|
|
* day
|
|
* @return CarbonImmutable The parsed date and time in user timezone as a CarbonImmutable object.
|
|
*
|
|
* @throws InvalidDateException
|
|
*/
|
|
public function parseUserDateTime(string $userDate, ?string $userTime = ''): CarbonImmutable
|
|
{
|
|
// Initialize result variable to null
|
|
$this->datetime = null;
|
|
|
|
// Validate input string
|
|
if (! $this->isValidDateString($userDate)) {
|
|
throw new InvalidDateException(
|
|
'The string is not a valid date time string to parse as user datetime string', $userDate
|
|
);
|
|
}
|
|
|
|
try {
|
|
|
|
// If no standard format worked, handle user format cases
|
|
$locale = substr($this->userLanguage, 0, 2);
|
|
$trimmedDate = trim($userDate);
|
|
|
|
if ($userTime === 'start') {
|
|
$this->datetime = CarbonImmutable::createFromLocaleFormat(
|
|
'!'.$this->userDateFormat,
|
|
$locale,
|
|
$trimmedDate,
|
|
$this->userTimezone
|
|
)
|
|
->startOfDay();
|
|
} elseif ($userTime === 'end') {
|
|
$this->datetime = CarbonImmutable::createFromLocaleFormat(
|
|
'!'.$this->userDateFormat,
|
|
$locale,
|
|
$trimmedDate,
|
|
$this->userTimezone
|
|
)
|
|
->endOfDay();
|
|
} elseif ($userTime === '' || $userTime === null) {
|
|
$this->datetime = CarbonImmutable::createFromLocaleFormat(
|
|
'!'.$this->userDateFormat,
|
|
$locale,
|
|
$trimmedDate,
|
|
$this->userTimezone
|
|
);
|
|
} else {
|
|
$this->datetime = CarbonImmutable::createFromLocaleFormat(
|
|
'!'.$this->userDateFormat.' '.$this->userTimeFormat,
|
|
$locale,
|
|
trim($trimmedDate.' '.$userTime),
|
|
$this->userTimezone
|
|
);
|
|
}
|
|
|
|
return $this->datetime;
|
|
|
|
} catch (\Exception $e) {
|
|
// Try standard formats first (non timezone formats are assumed user timezone)
|
|
$standardFormat = $this->tryParseStandardFormats($userDate, $this->userTimezone);
|
|
if ($standardFormat !== false) {
|
|
$this->datetime = $standardFormat;
|
|
|
|
return $this->datetime;
|
|
}
|
|
|
|
throw new InvalidFormatException('The string is not a valid date time string to parse as user datetime string');
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Parses a database date string and returns a CarbonImmutable instance.
|
|
*
|
|
* @param string $dbDate The date string in the database format to parse.
|
|
* @return CarbonImmutable The parsed CarbonImmutable instance in db timezone (UTC)
|
|
*
|
|
* @throws InvalidDateException
|
|
*/
|
|
public function parseDbDateTime(string $dbDate): CarbonImmutable
|
|
{
|
|
if (! $this->isValidDateString($dbDate)) {
|
|
throw new InvalidDateException('The string is not a valid date time string to parse as Database string', $dbDate);
|
|
}
|
|
|
|
try {
|
|
$this->datetime = CarbonImmutable::createFromFormat($this->dbFormat, $dbDate, $this->dbTimezone)->locale(
|
|
$this->userLanguage
|
|
);
|
|
|
|
return $this->datetime;
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
// Try standard formats first (non timezone formats are assumed user timezone)
|
|
$standardFormat = $this->tryParseStandardFormats($dbDate, $this->dbTimezone);
|
|
if ($standardFormat !== false) {
|
|
$this->datetime = $standardFormat;
|
|
|
|
return $this->datetime;
|
|
}
|
|
|
|
throw new InvalidFormatException('The string is not a valid date time string to parse as Database string');
|
|
}
|
|
}
|
|
|
|
protected function tryParseStandardFormats($userDate, $timezone): CarbonImmutable|false
|
|
{
|
|
// Define standard formats to try first
|
|
$standardFormats = [
|
|
"Y-m-d\TH:i:sP", // ISO 8601 with timezone offset (e.g., 2025-04-16T00:00:00-04:00)
|
|
"Y-m-d\TH:i:s\Z", // ISO 8601 UTC/Zulu time (e.g., 2025-04-16T00:00:00Z)
|
|
'Y-m-d H:i:s',
|
|
DateTime::ATOM,
|
|
DateTime::ISO8601,
|
|
DateTime::W3C,
|
|
"Ymd\THis\Z", // ISO 8601 UTC/Zulu time (e.g., 20250429T110000Z) (for ical)
|
|
"Ymd\THis", // ISO 8601 no timezone (e.g., 20250429T110000) (for ical)
|
|
"Y-m-d\TH:i:s", // ISO 8601 without timezone (e.g., 2025-04-16T00:00:00)
|
|
"Y-m-d\TH:i:se",
|
|
'Y-m-d',
|
|
'Y-m-d H:i',
|
|
];
|
|
|
|
// Added in PHP 8.2
|
|
if (defined('DateTime::ISO8601_EXPANDED')) {
|
|
$standardFormats[] = DateTime::ISO8601_EXPANDED;
|
|
}
|
|
|
|
// Try standard formats first
|
|
foreach ($standardFormats as $format) {
|
|
try {
|
|
|
|
// If dates provided don't timezone informaiton, we assume it's in user datetime
|
|
if ($format === 'Y-m-d') {
|
|
return CarbonImmutable::createFromFormat($format, $userDate, $timezone)->midDay();
|
|
}
|
|
|
|
if ($format === 'Y-m-d H:i:s' || $format === 'Y-m-d H:i' || $format === "Ymd\THis") {
|
|
return CarbonImmutable::createFromFormat($format, $userDate, $timezone);
|
|
} else {
|
|
return CarbonImmutable::createFromFormat($format, $userDate, $timezone);
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Continue to next format
|
|
}
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
/**
|
|
* Parses a user 24-hour time string and returns a CarbonImmutable instance.
|
|
*
|
|
* @param string $local24Time The 24-hour time string to parse.
|
|
* @return CarbonImmutable The parsed CarbonImmutable instance in the user's timezone
|
|
*/
|
|
public function parseUser24hTime(string $local24Time): CarbonImmutable
|
|
{
|
|
$this->datetime = CarbonImmutable::createFromFormat('!H:i', $local24Time, $this->userTimezone);
|
|
|
|
return $this->datetime;
|
|
}
|
|
|
|
/**
|
|
* Parses a database 24-hour time string and returns a CarbonImmutable instance.
|
|
*
|
|
* @param string $db24Time The 24-hour time string to parse.
|
|
* @return CarbonImmutable The parsed CarbonImmutable instance in the database timezone (UTC)
|
|
*/
|
|
public function parseDb24hTime(string $db24Time): CarbonImmutable
|
|
{
|
|
$this->datetime = CarbonImmutable::createFromFormat('!H:i', $db24Time, $this->dbTimezone);
|
|
|
|
return $this->datetime;
|
|
}
|
|
|
|
/**
|
|
* Returns the current date and time based on the user's timezone and language.
|
|
*
|
|
* @return CarbonImmutable The current date and time in the user's timezone and language.
|
|
*/
|
|
public function userNow(): CarbonImmutable
|
|
{
|
|
return CarbonImmutable::now($this->userTimezone)->locale($this->userLanguage);
|
|
}
|
|
|
|
/**
|
|
* Returns the current date and time in the database timezone as a CarbonImmutable instance.
|
|
*
|
|
* @return CarbonImmutable The current date and time in the database timezone (UTC) as a CarbonImmutable instance.
|
|
*/
|
|
public function dbNow(): CarbonImmutable
|
|
{
|
|
return CarbonImmutable::now($this->dbTimezone)->locale($this->userLanguage);
|
|
}
|
|
|
|
/**
|
|
* Sets the CarbonImmutable date for the current instance.
|
|
*
|
|
* @param CarbonImmutable|Carbon $date The CarbonImmutable or Carbon instance to set the date.
|
|
* @return string|CarbonImmutable|false
|
|
*/
|
|
public function setCarbonDate(CarbonImmutable|Carbon $date): string|CarbonImmutable|bool
|
|
{
|
|
return $this->datetime = CarbonImmutable::create($date)->locale($this->userLanguage);
|
|
}
|
|
|
|
/**
|
|
* isValidDateString - checks if a given string is a valid date and time string
|
|
*
|
|
* @param ?string $dateTimeString The date and time string to be validated
|
|
* @return bool Returns true if the string is a valid string that is worth sending to a parser, false otherwise
|
|
*/
|
|
public function isValidDateString(?string $dateTimeString): bool
|
|
{
|
|
return empty($dateTimeString) === false
|
|
&& $dateTimeString !== '1969-12-31 00:00:00'
|
|
&& $dateTimeString !== '0000-00-00 00:00:00';
|
|
}
|
|
}
|