friendship ended with social-app. php is my new best friend
1<?php
2
3/**
4 * This file is part of the Nette Framework (https://nette.org)
5 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6 */
7
8declare(strict_types=1);
9
10namespace Nette\Utils;
11
12use Nette;
13use function array_merge, checkdate, implode, is_numeric, is_string, preg_replace_callback, sprintf, time, trim;
14
15
16/**
17 * DateTime.
18 */
19class DateTime extends \DateTime implements \JsonSerializable
20{
21 use Nette\SmartObject;
22
23 /** minute in seconds */
24 public const MINUTE = 60;
25
26 /** hour in seconds */
27 public const HOUR = 60 * self::MINUTE;
28
29 /** day in seconds */
30 public const DAY = 24 * self::HOUR;
31
32 /** week in seconds */
33 public const WEEK = 7 * self::DAY;
34
35 /** average month in seconds */
36 public const MONTH = 2_629_800;
37
38 /** average year in seconds */
39 public const YEAR = 31_557_600;
40
41
42 /**
43 * Creates a DateTime object from a string, UNIX timestamp, or other DateTimeInterface object.
44 * @throws \Exception if the date and time are not valid.
45 */
46 public static function from(string|int|\DateTimeInterface|null $time): static
47 {
48 if ($time instanceof \DateTimeInterface) {
49 return static::createFromInterface($time);
50
51 } elseif (is_numeric($time)) {
52 if ($time <= self::YEAR) {
53 $time += time();
54 }
55
56 return (new static)->setTimestamp((int) $time);
57
58 } else { // textual or null
59 return new static((string) $time);
60 }
61 }
62
63
64 /**
65 * Creates DateTime object.
66 * @throws Nette\InvalidArgumentException if the date and time are not valid.
67 */
68 public static function fromParts(
69 int $year,
70 int $month,
71 int $day,
72 int $hour = 0,
73 int $minute = 0,
74 float $second = 0.0,
75 ): static
76 {
77 $s = sprintf('%04d-%02d-%02d %02d:%02d:%02.5F', $year, $month, $day, $hour, $minute, $second);
78 if (
79 !checkdate($month, $day, $year)
80 || $hour < 0 || $hour > 23
81 || $minute < 0 || $minute > 59
82 || $second < 0 || $second >= 60
83 ) {
84 throw new Nette\InvalidArgumentException("Invalid date '$s'");
85 }
86
87 return new static($s);
88 }
89
90
91 /**
92 * Returns a new DateTime object formatted according to the specified format.
93 */
94 public static function createFromFormat(
95 string $format,
96 string $datetime,
97 string|\DateTimeZone|null $timezone = null,
98 ): static|false
99 {
100 if (is_string($timezone)) {
101 $timezone = new \DateTimeZone($timezone);
102 }
103
104 $date = parent::createFromFormat($format, $datetime, $timezone);
105 return $date ? static::from($date) : false;
106 }
107
108
109 public function __construct(string $datetime = 'now', ?\DateTimeZone $timezone = null)
110 {
111 $this->apply($datetime, $timezone, true);
112 }
113
114
115 public function modify(string $modifier): static
116 {
117 $this->apply($modifier);
118 return $this;
119 }
120
121
122 public function setDate(int $year, int $month, int $day): static
123 {
124 if (!checkdate($month, $day, $year)) {
125 trigger_error(sprintf(self::class . ': The date %04d-%02d-%02d is not valid.', $year, $month, $day), E_USER_WARNING);
126 }
127 return parent::setDate($year, $month, $day);
128 }
129
130
131 public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): static
132 {
133 if (
134 $hour < 0 || $hour > 23
135 || $minute < 0 || $minute > 59
136 || $second < 0 || $second >= 60
137 || $microsecond < 0 || $microsecond >= 1_000_000
138 ) {
139 trigger_error(sprintf(self::class . ': The time %02d:%02d:%08.5F is not valid.', $hour, $minute, $second + $microsecond / 1_000_000), E_USER_WARNING);
140 }
141 return parent::setTime($hour, $minute, $second, $microsecond);
142 }
143
144
145 /**
146 * Converts a relative time string (e.g. '10 minut') to seconds.
147 */
148 public static function relativeToSeconds(string $relativeTime): int
149 {
150 return (new self('@0 ' . $relativeTime))
151 ->getTimestamp();
152 }
153
154
155 private function apply(string $datetime, $timezone = null, bool $ctr = false): void
156 {
157 $relPart = '';
158 $absPart = preg_replace_callback(
159 '/[+-]?\s*\d+\s+((microsecond|millisecond|[mµu]sec)s?|[mµ]s|sec(ond)?s?|min(ute)?s?|hours?)(\s+ago)?\b/iu',
160 function ($m) use (&$relPart) {
161 $relPart .= $m[0] . ' ';
162 return '';
163 },
164 $datetime,
165 );
166
167 if ($ctr) {
168 parent::__construct($absPart, $timezone);
169 $this->handleErrors($datetime);
170 } elseif (trim($absPart)) {
171 parent::modify($absPart) && $this->handleErrors($datetime);
172 }
173
174 if ($relPart) {
175 $timezone ??= $this->getTimezone();
176 $this->setTimezone(new \DateTimeZone('UTC'));
177 parent::modify($relPart) && $this->handleErrors($datetime);
178 $this->setTimezone($timezone);
179 }
180 }
181
182
183 /**
184 * Returns JSON representation in ISO 8601 (used by JavaScript).
185 */
186 public function jsonSerialize(): string
187 {
188 return $this->format('c');
189 }
190
191
192 /**
193 * Returns the date and time in the format 'Y-m-d H:i:s'.
194 */
195 public function __toString(): string
196 {
197 return $this->format('Y-m-d H:i:s');
198 }
199
200
201 /**
202 * You'd better use: (clone $dt)->modify(...)
203 */
204 public function modifyClone(string $modify = ''): static
205 {
206 $dolly = clone $this;
207 return $modify ? $dolly->modify($modify) : $dolly;
208 }
209
210
211 private function handleErrors(string $value): void
212 {
213 $errors = self::getLastErrors();
214 $errors = array_merge($errors['errors'] ?? [], $errors['warnings'] ?? []);
215 if ($errors) {
216 trigger_error(self::class . ': ' . implode(', ', $errors) . " '$value'", E_USER_WARNING);
217 }
218 }
219}