format('Y-m-d H:i:s.u'); } elseif ( is_numeric($datetime) || str_starts_with((string) $datetime, '@') ) { $datetime = '@' . ltrim((string) $datetime, '@'); } parent::__construct($datetime, $timezone); } /** * Gets the WordPress default timezone. * * @return DateTimeZone */ public static function getDefaultTimezone() { $tz = wp_timezone(); return $tz instanceof DateTimeZone ? $tz : new DateTimeZone($tz); } /** * Creates a DateTime object from various inputs. * * @param string|int|DateTimeInterface $time * @param string|DateTimeZone|null $tz * * @return static * @throws \InvalidArgumentException */ public static function create($time = null, $tz = null) { if (is_null($tz)) { $timezone = (new static)->getDefaultTimezone(); } else { $timezone = is_string($tz) ? new DateTimeZone($tz) : $tz; if (!$timezone instanceof DateTimeZone) { throw new InvalidArgumentException('Invalid timezone.'); } } // Handle DateTimeInterface directly if ($time instanceof DateTimeInterface) { $dateTime = new static( $time->format('Y-m-d H:i:s.u'), $time->getTimezone() ); // Override timezone if explicitly provided if ($tz !== null) { $dateTime = $dateTime->setTimezone($timezone); } return $dateTime; } // Handle numeric timestamps if (is_numeric($time)) { // Treat small numbers as offsets from now (optional) if ($time <= 31556952) { // ~seconds in a year $time += time(); } // create from timestamp $dateTime = new static('@' . $time); return $dateTime->setTimezone($timezone); } // Fallback: treat as string or null ('now') $dateTime = new static($time ?: 'now', $timezone); return $dateTime; } /** * Creates a new instance for the current time. * * @param string|DateTimeZone|null $tz * @return static */ public static function now($tz = null) { return static::create('now', $tz); } /** * Creates a new instance for the beginning of today. * * @param string|DateTimeZone|null $tz * @return static */ public static function today($tz = null) { return static::create('today', $tz)->startOfDay(); } /** * Creates a new instance for the beginning of yesterday. * * @param string|DateTimeZone|null $tz * @return static */ public static function yesterday($tz = null) { return static::now($tz)->modify('-1 day')->startOfDay(); } /** * Creates a new instance for the beginning of tomorrow. * * @param string|DateTimeZone|null $tz * @return static */ public static function tomorrow($tz = null) { return static::now($tz)->modify('+1 day')->startOfDay(); } /** * Creates a new instance for the beginning of the current week. * * @param string|DateTimeZone|null $tz * @return static */ public static function currentWeek($tz = null) { return static::now($tz)->startOfWeek(); } /** * Creates a new instance for the beginning of last week. * * @param string|DateTimeZone|null $tz * @return static */ public static function lastWeek($tz = null) { return static::now($tz)->subWeek()->startOfWeek(); } /** * Creates a new instance for the beginning of next week. * * @param string|DateTimeZone|null $tz * @return static */ public static function nextWeek($tz = null) { return static::now($tz)->addWeek()->startOfWeek(); } /** * Creates a new instance for the beginning of the current month. * * @param string|DateTimeZone|null $tz * @return static */ public static function currentMonth($tz = null) { return static::now($tz)->startOfMonth(); } /** * Creates a new instance for the beginning of last month. * * @param string|DateTimeZone|null $tz * @return static */ public static function lastMonth($tz = null) { return static::now($tz)->subMonth()->startOfMonth(); } /** * Creates a new instance for the beginning of next month. * * @param string|DateTimeZone|null $tz * @return static */ public static function nextMonth($tz = null) { return static::now($tz)->addMonth()->startOfMonth(); } /** * Creates a new instance for the beginning of the current year. * * @param string|DateTimeZone|null $tz * @return static */ public static function currentYear($tz = null) { return static::now($tz)->startOfYear(); } /** * Creates a new instance for the beginning of last year. * * @param string|DateTimeZone|null $tz * @return static */ public static function lastYear($tz = null) { return static::now($tz)->subYear()->startOfYear(); } /** * Creates a new instance for the beginning of next year. * * @param string|DateTimeZone|null $tz * @return static */ public static function nextYear($tz = null) { return static::now($tz)->addYear()->startOfYear(); } /** * Sets the timezone for the instance. * * @param string|DateTimeZone $tz * @return self */ public function timezone($tz) { if (is_string($tz)) { $tz = new DateTimeZone($tz); } return $this->setTimezone($tz); } /** * Checks if the current instance is between two dates. * * @param string|DateTimeInterface $date1 * @param string|DateTimeInterface $date2 * @return bool */ public function between($date1, $date2) { $date1 = $date1 instanceof DateTimeInterface ? $date1 : new static($date1); $date2 = $date2 instanceof DateTimeInterface ? $date2 : new static($date2); if ($date1 > $date2) { [$date1, $date2] = [$date2, $date1]; } return ($this >= $date1 && $this <= $date2); } /** * Creates a new instance from a formatted string. * * @param string $format * @param string $datetimeString * @param string|DateTimeZone|null $timezone * * @return static * @throws \InvalidArgumentException */ public static function createFromFormat( $format, $datetimeString, $timezone = null ) { if (is_null($timezone)) { $timezone = (new static)->getDefaultTimezone(); } else { $timezone = is_string($timezone) ? new DateTimeZone($timezone) : $timezone; } if (!$timezone instanceof DateTimeZone) { throw new InvalidArgumentException('Invalid timezone.'); } $dateTime = PHPDateTimeImmutable::createFromFormat( $format, $datetimeString ); if ($dateTime !== false) { if (!$dateTime instanceof static) { return new static( $dateTime->format(ltrim($format, '!')), $timezone ); } $dateTime->setTimezone($timezone); return $dateTime; } throw new InvalidArgumentException( "Unable to create datetime from: {$datetimeString}." ); } /** * Creates a new instance from date parts. * * @param int $year * @param int $month * @param int $day * @param int $hour * @param int $minute * @param int $second * @param string|DateTimeZone|null $tz * * @return static * @throws \InvalidArgumentException */ public static function createFromDate( int $year, int $month, int $day, int $hour = 0, int $minute = 0, int $second = 0, $tz = null ) { if ( !checkdate($month, $day, $year) || $hour < 0 || $hour > 23 || $minute < 0 || $minute > 59 || $second < 0 || $second > 59 ) { throw new InvalidArgumentException( sprintf("Invalid date '%04d-%02d-%02d %02d:%02d:%02d'", $year, $month, $day, $hour, $minute, $second) ); } return new static(sprintf('%04d-%02d-%02d %02d:%02d:%02d', $year, $month, $day, $hour, $minute, $second), $tz); } /** * Creates a new instance from a UTC date string. * * @param string $dateString * @param string $format * @return static */ public static function createFromUTC( string $dateString, string $format = 'Y-m-d H:i:s' ) { $gmtDate = get_date_from_gmt($dateString, $format); return new static($gmtDate, static::getDefaultTimezone()); } /** * Parses a datetime string and returns a new instance. * * @param string $datetimeString * @param string|DateTimeZone|null $timezone * * @return static * @throws \InvalidArgumentException */ public static function parse($datetimeString, $timezone = null) { try { return new static($datetimeString, $timezone); } catch (Exception $e) { throw new InvalidArgumentException( 'Unable to handle datetime.', 0, $e ); } } /** * Adds an interval to the instance. * * @param DateInterval|string $interval * @return self * @throws \InvalidArgumentException */ public function add($interval) { if ($interval instanceof DateInterval) { return parent::add($interval); } if (!is_string($interval) || trim($interval) === '') { throw new InvalidArgumentException('Invalid interval string.'); } return $this->modify('+' . $interval); } /** * Subtracts an interval from the instance. * * @param DateInterval|string $interval * @return self * @throws \InvalidArgumentException */ public function sub($interval) { if ($interval instanceof DateInterval) { return parent::sub($interval); } if (!is_string($interval) || trim($interval) === '') { throw new InvalidArgumentException('Invalid interval string.'); } return $this->modify('-' . $interval); } /** * Creates a copy of the instance. * * @return self */ public function copy() { return clone $this; } /** * Converts the instance to a string. * * @return string */ public function __toString() { return $this->format('Y-m-d H:i:s'); } /** * Checks if the instance has a timezone. * * @return bool */ public function hasTimezone() { return $this->getTimezone() instanceof DateTimeZone; } /** * Sets the time to the beginning of the day. * * @return self */ public function startOfDay() { return $this->setTime(0, 0, 0, 0); } /** * Sets the time to the end of the day. * * @return self */ public function endOfDay() { return $this->setTime(23, 59, 59, 999999); } /** * Sets the time to the beginning of the hour. * * @return self */ public function startOfHour() { return $this->setTime((int)$this->format('H'), 0, 0, 0); } /** * Sets the time to the end of the hour. * * @return self */ public function endOfHour() { return $this->setTime((int)$this->format('H'), 59, 59, 999999); } /** * Sets the time to the beginning of the minute. * * @return self */ public function startOfMinute() { return $this->setTime( (int)$this->format('H'), (int)$this->format('i'), 0, 0 ); } /** * Sets the time to the end of the minute. * * @return self */ public function endOfMinute() { return $this->setTime( (int)$this->format('H'), (int)$this->format('i'), 59, 999999 ); } /** * Sets the date to the beginning of the week. * * @return self */ public function startOfWeek() { $start = max(0, min(6, (int)get_option('start_of_week'))); $day = (int)$this->format('w'); $diff = $day - $start; if ($diff < 0) $diff += 7; return $this->sub(new DateInterval("P{$diff}D"))->startOfDay(); } /** * Sets the date to the end of the week. * * @return self */ public function endOfWeek() { $start = max(0, min(6, (int)get_option('start_of_week'))); $day = (int)$this->format('w'); $end = ($start + 6) % 7; $diff = $end - $day; if ($diff < 0) $diff += 7; return $this->add(new DateInterval("P{$diff}D"))->endOfDay(); } /** * Sets the date to the beginning of the month. * * @return self */ public function startOfMonth() { return $this->setDate( (int)$this->format('Y'), (int)$this->format('m'), 1 )->startOfDay(); } /** * Sets the date to the end of the month. * * @return self */ public function endOfMonth() { return $this->modify('last day of this month')->endOfDay(); } /** * Sets the date to the beginning of the quarter. * * @return self */ public function startOfQuarter() { $month = (int)$this->format('m'); $startMonth = $month <= 3 ? 1 : ( $month <= 6 ? 4 : ($month <= 9 ? 7 : 10) ); return $this->setDate( (int)$this->format('Y'), $startMonth, 1 )->startOfDay(); } /** * Sets the date to the end of the quarter. * * @return self */ public function endOfQuarter() { $month = (int)$this->format('m'); $endMonth = $month <= 3 ? 3 : ($month <= 6 ? 6 : ($month <= 9 ? 9 : 12)); $lastDay = (int)date('t', strtotime( "{$this->format('Y')}-{$endMonth}-01" )); return $this->setDate( (int)$this->format('Y'), $endMonth, $lastDay )->endOfDay(); } /** * Sets the date to the beginning of the year. * * @return self */ public function startOfYear() { return $this->setDate( (int)$this->format('Y'), 1, 1 )->startOfDay(); } /** * Sets the date to the end of the year. * * @return self */ public function endOfYear() { return $this->setDate( (int)$this->format('Y'), 12, 31 )->endOfDay(); } /** * Sets the date to the beginning of the decade. * * @return self */ public function startOfDecade() { $year = (int)$this->format('Y'); $start = $year - ($year % 10); return $this->setDate($start, 1, 1)->startOfDay(); } /** * Sets the date to the end of the decade. * * @return self */ public function endOfDecade() { $year = (int)$this->format('Y'); $end = $year + (9 - ($year % 10)); return $this->setDate($end, 12, 31)->endOfDay(); } /** * Magic setter for time units. * * @param string $key * @param mixed $value * @return self * @throws \InvalidArgumentException */ public function __set($key, $value) { $new = clone $this; switch ($key) { case 'year': return $new->setDate( (int)$value, (int)$this->format('m'), (int)$this->format('d') ); case 'month': return $new->setDate( (int)$this->format('Y'), (int)$value, (int)$this->format('d') ); case 'day': return $new->setDate( (int)$this->format('Y'), (int)$this->format('m'), (int)$value ); case 'hour': return $new->setTime( (int)$value, (int)$this->format('i'), (int)$this->format('s') ); case 'minute': return $new->setTime( (int)$this->format('H'), (int)$value, (int)$this->format('s') ); case 'second': return $new->setTime( (int)$this->format('H'), (int)$this->format('i'), (int)$value ); default: throw new InvalidArgumentException( "Cannot set unknown property '{$key}'." ); } } /** * Magic method for dynamic calls (e.g., `addDay`, `getYear`). * * @param string $method * @param array $params * @return mixed|self * @throws \InvalidArgumentException */ public function __call($method, $params) { // set* methods if (strpos($method, 'set') === 0) { $unit = lcfirst(substr($method, 3)); if ($params && in_array($unit, static::SINGULAR_UNITS, true)) { return $this->__set($unit, reset($params)); } } // get* methods if (strpos($method, 'get') === 0) { $unit = lcfirst(substr($method, 3)); switch ($unit) { case 'year': return (int)$this->format('Y'); case 'month': return (int)$this->format('m'); case 'day': return (int)$this->format('d'); case 'hour': return (int)$this->format('H'); case 'minute': return (int)$this->format('i'); case 'second': return (int)$this->format('s'); case 'quarter': return (int)(ceil((int)$this->format('m') / 3)); case 'decade': return (int)floor((int)$this->format('Y') / 10) * 10; default: throw new InvalidArgumentException( "Call to undefined method {$method}." ); } } // add*/sub* methods $action = null; if (strpos($method, 'add') === 0) $action = '+'; elseif (strpos($method, 'sub') === 0) $action = '-'; if ($action) { $duration = $params[0] ?? 1; $unit = lcfirst(substr($method, 3)); if ( !in_array( $unit, array_merge( static::SINGULAR_UNITS, static::PLURAL_UNITS ), true ) ) { throw new InvalidArgumentException( "Call to undefined method {$method}." ); } // Special handling for quarter (3 months) if ($unit === 'quarter' || $unit === 'quarters') $unit = 'month'; // Special handling for decade (10 years) if ($unit === 'decade' || $unit === 'decades') $unit = 'year'; $multiplier = 1; if (stripos($method, 'quarter') !== false) $multiplier = 3; if (stripos($method, 'decade') !== false) $multiplier = 10; return $this->modify( "{$action}" . ($duration * $multiplier) . " {$unit}" ); } throw new InvalidArgumentException("Call to undefined method {$method}."); } }