1 | <?php |
||||
2 | |||||
3 | namespace BFW\Helpers; |
||||
4 | |||||
5 | use \DateTime; |
||||
6 | use \Exception; |
||||
7 | |||||
8 | /** |
||||
9 | * Class to have shortcuts to DateTime(Zone) methods and to display a date |
||||
10 | * with words and not only numbers (today, yesterday, since... etc). |
||||
11 | */ |
||||
12 | class Dates extends DateTime |
||||
13 | { |
||||
14 | /** |
||||
15 | * @var string[] $humanReadableI18n Words used in method to transform |
||||
16 | * date difference to human readable. |
||||
17 | */ |
||||
18 | protected static $humanReadableI18n = [ |
||||
19 | 'now' => 'now', |
||||
20 | 'today_past' => '{time} ago', |
||||
21 | 'today_future' => 'in {time}', |
||||
22 | 'yesterday' => 'yesterday', |
||||
23 | 'tomorrow' => 'tomorrow', |
||||
24 | 'others' => 'the {date}', |
||||
25 | 'time_part' => ' at {time}' |
||||
26 | ]; |
||||
27 | |||||
28 | /** |
||||
29 | * @var string[] $humanReadableFormats Date and time formats used in |
||||
30 | * method to transform date difference to human readable. |
||||
31 | */ |
||||
32 | protected static $humanReadableFormats = [ |
||||
33 | 'dateSameYear' => 'm-d', |
||||
34 | 'dateDifferentYear' => 'Y-m-d', |
||||
35 | 'time' => 'H:i' |
||||
36 | ]; |
||||
37 | |||||
38 | /** |
||||
39 | * Return the value of the humanReadableI18n property |
||||
40 | * |
||||
41 | * @return string[] |
||||
42 | */ |
||||
43 | public static function getHumanReadableI18n(): array |
||||
44 | { |
||||
45 | return self::$humanReadableI18n; |
||||
46 | } |
||||
47 | |||||
48 | /** |
||||
49 | * Define a new value for a key of the humanReadableI18n property |
||||
50 | * |
||||
51 | * @param string $key The key in humanReadableI18n |
||||
52 | * @param string $value The new value for the key |
||||
53 | * |
||||
54 | * @return void |
||||
55 | */ |
||||
56 | public static function setHumanReadableI18nKey(string $key, string $value) |
||||
57 | { |
||||
58 | self::$humanReadableI18n[$key] = $value; |
||||
59 | } |
||||
60 | |||||
61 | /** |
||||
62 | * Define a new value to the property humanReadableI18n |
||||
63 | * |
||||
64 | * @param string[] $value The new value for the property |
||||
65 | * |
||||
66 | * @return void |
||||
67 | */ |
||||
68 | public static function setHumanReadableI18n(array $value) |
||||
69 | { |
||||
70 | self::$humanReadableI18n = $value; |
||||
71 | } |
||||
72 | |||||
73 | /** |
||||
74 | * Return the value of the humanReadableFormats property |
||||
75 | * |
||||
76 | * @return string[] |
||||
77 | */ |
||||
78 | public static function getHumanReadableFormats(): array |
||||
79 | { |
||||
80 | return self::$humanReadableFormats; |
||||
81 | } |
||||
82 | |||||
83 | /** |
||||
84 | * Define a new value for a key of the humanReadableFormats property |
||||
85 | * |
||||
86 | * @param string $key The key in humanReadableFormats |
||||
87 | * @param string $value The new value for the key |
||||
88 | * |
||||
89 | * @return void |
||||
90 | */ |
||||
91 | public static function setHumanReadableFormatsKey( |
||||
92 | string $key, |
||||
93 | string $value |
||||
94 | ) { |
||||
95 | self::$humanReadableFormats[$key] = $value; |
||||
96 | } |
||||
97 | |||||
98 | /** |
||||
99 | * Define a new value to the property humanReadableFormats |
||||
100 | * |
||||
101 | * @param string[] $value The new value for the property |
||||
102 | * |
||||
103 | * @return void |
||||
104 | */ |
||||
105 | public static function setHumanReadableFormats(array $value) |
||||
106 | { |
||||
107 | self::$humanReadableFormats = $value; |
||||
108 | } |
||||
109 | |||||
110 | /** |
||||
111 | * Return the date. Format is Y-m-d H:i:sO |
||||
112 | * |
||||
113 | * @return string |
||||
114 | */ |
||||
115 | public function getDate(): string |
||||
116 | { |
||||
117 | return parent::format('Y-m-d H:i:sO'); |
||||
118 | } |
||||
119 | |||||
120 | /** |
||||
121 | * Return a numeric representation of a year, 4 digits. |
||||
122 | * |
||||
123 | * @return int |
||||
124 | */ |
||||
125 | public function getYear(): int |
||||
126 | { |
||||
127 | return (int) parent::format('Y'); |
||||
128 | } |
||||
129 | |||||
130 | /** |
||||
131 | * Return the numeric representation of a month, without leading zeros. |
||||
132 | * The returned int format can not have leading zeros. |
||||
133 | * |
||||
134 | * @return int |
||||
135 | */ |
||||
136 | public function getMonth(): int |
||||
137 | { |
||||
138 | return (int) parent::format('m'); |
||||
139 | } |
||||
140 | |||||
141 | /** |
||||
142 | * Return the day of the month without leading zeros. |
||||
143 | * The returned int format can not have leading zeros. |
||||
144 | * |
||||
145 | * @return int |
||||
146 | */ |
||||
147 | public function getDay(): int |
||||
148 | { |
||||
149 | return (int) parent::format('d'); |
||||
150 | } |
||||
151 | |||||
152 | /** |
||||
153 | * Return 24-hour format without leading zeros. |
||||
154 | * The returned int format can not have leading zeros. |
||||
155 | * |
||||
156 | * @return int |
||||
157 | */ |
||||
158 | public function getHour(): int |
||||
159 | { |
||||
160 | return (int) parent::format('H'); |
||||
161 | } |
||||
162 | |||||
163 | /** |
||||
164 | * Return minutes, without leading zeros. |
||||
165 | * The returned int format can not have leading zeros. |
||||
166 | * |
||||
167 | * @return int |
||||
168 | */ |
||||
169 | public function getMinute(): int |
||||
170 | { |
||||
171 | return (int) parent::format('i'); |
||||
172 | } |
||||
173 | |||||
174 | /** |
||||
175 | * Return second, without leading zeros. |
||||
176 | * The returned int format can not have leading zeros. |
||||
177 | * |
||||
178 | * @return int |
||||
179 | */ |
||||
180 | public function getSecond(): int |
||||
181 | { |
||||
182 | return (int) parent::format('s'); |
||||
183 | } |
||||
184 | |||||
185 | /** |
||||
186 | * Return the difference to Greenwich time (GMT) |
||||
187 | * with colon between hours and minutes |
||||
188 | * |
||||
189 | * @return string |
||||
190 | */ |
||||
191 | public function getZone(): string |
||||
192 | { |
||||
193 | return parent::format('P'); |
||||
194 | } |
||||
195 | |||||
196 | /** |
||||
197 | * Return date's SQL format (postgresql format). |
||||
198 | * The return can be an array or a string. |
||||
199 | * |
||||
200 | * @param boolean $returnArray (default false) True to return an array. |
||||
201 | * @param boolean $withZone (default false) True to include the timezone |
||||
202 | * into the time returned data. |
||||
203 | * |
||||
204 | * @return string[]|string |
||||
205 | */ |
||||
206 | public function getSqlFormat( |
||||
207 | bool $returnArray = false, |
||||
208 | bool $withZone = false |
||||
209 | ) { |
||||
210 | $date = $this->format('Y-m-d'); |
||||
211 | $time = $this->format('H:i:s'); |
||||
212 | |||||
213 | if ($withZone === true) { |
||||
214 | $time .= $this->format('O'); |
||||
215 | } |
||||
216 | |||||
217 | if ($returnArray) { |
||||
218 | return [$date, $time]; |
||||
219 | } |
||||
220 | |||||
221 | return $date.' '.$time; |
||||
222 | } |
||||
223 | |||||
224 | /** |
||||
225 | * List all timezone existing in current php version |
||||
226 | * |
||||
227 | * @return string[] |
||||
228 | */ |
||||
229 | public function lstTimeZone(): array |
||||
230 | { |
||||
231 | return parent::getTimezone()->listIdentifiers(); |
||||
232 | } |
||||
233 | |||||
234 | /** |
||||
235 | * List all continent define in php DateTimeZone. |
||||
236 | * |
||||
237 | * @return string[] |
||||
238 | */ |
||||
239 | public function lstTimeZoneContinent(): array |
||||
240 | { |
||||
241 | return [ |
||||
242 | 'Africa', |
||||
243 | 'America', |
||||
244 | 'Antartica', |
||||
245 | 'Arctic', |
||||
246 | 'Asia', |
||||
247 | 'Atlantic', |
||||
248 | 'Australia', |
||||
249 | 'Europe', |
||||
250 | 'Indian', |
||||
251 | 'Pacific' |
||||
252 | ]; |
||||
253 | } |
||||
254 | |||||
255 | /** |
||||
256 | * List all available country for a continent |
||||
257 | * |
||||
258 | * @param string $continent The continent for which we want |
||||
259 | * the countries list |
||||
260 | * |
||||
261 | * @return string[] |
||||
262 | */ |
||||
263 | public function lstTimeZoneCountries(string $continent): array |
||||
264 | { |
||||
265 | $allCountries = $this->lstTimeZone(); |
||||
266 | $countries = []; |
||||
267 | |||||
268 | foreach ($allCountries as $country) { |
||||
269 | if (strpos($country, $continent) !== false) { |
||||
270 | $countries[] = $country; |
||||
271 | } |
||||
272 | } |
||||
273 | |||||
274 | return $countries; |
||||
275 | } |
||||
276 | |||||
277 | /** |
||||
278 | * Transform a date to a human readable format |
||||
279 | * |
||||
280 | * @param boolean $returnDateAndTime (default true) True to return date and |
||||
281 | * time concatenated with a space. False to have only date. |
||||
282 | * |
||||
283 | * @return string |
||||
284 | */ |
||||
285 | public function humanReadable(bool $returnDateAndTime = true): string |
||||
286 | { |
||||
287 | $current = new Dates; |
||||
288 | $diff = parent::diff($current); |
||||
289 | |||||
290 | $parsedTxt = new class { |
||||
291 | public $date = ''; |
||||
292 | public $time = ''; |
||||
293 | }; |
||||
294 | |||||
295 | if ($current == $this) { |
||||
296 | //Now |
||||
297 | $this->humanDateNow($parsedTxt); |
||||
298 | } elseif ( |
||||
299 | $this->humanDateIsYesterdayOrTomorrow($diff, $current) === true |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
300 | ) { |
||||
301 | if ($diff->invert === 0) { |
||||
302 | $this->humanDateYesterday($parsedTxt); //Yesterday |
||||
303 | } else { |
||||
304 | $this->humanDateTomorrow($parsedTxt); //Tomorrow |
||||
305 | } |
||||
306 | } elseif ($diff->days === 0) { |
||||
307 | //Today |
||||
308 | $this->humanDateToday($parsedTxt, $diff); |
||||
0 ignored issues
–
show
It seems like
$diff can also be of type false ; however, parameter $diff of BFW\Helpers\Dates::humanDateToday() does only seem to accept DateInterval , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
309 | } else { |
||||
310 | $this->humanDateOther($parsedTxt, $current); |
||||
311 | } |
||||
312 | |||||
313 | $txtReturned = $parsedTxt->date; |
||||
314 | if ($returnDateAndTime === true && $parsedTxt->time !== '') { |
||||
315 | $txtReturned .= $parsedTxt->time; |
||||
316 | } |
||||
317 | |||||
318 | return $txtReturned; |
||||
319 | } |
||||
320 | |||||
321 | /** |
||||
322 | * Check if the date to read for humanReadable is yesterday or tomorrow. |
||||
323 | * |
||||
324 | * We cannot only check the property "d" of DateInterval because it's a |
||||
325 | * range of +/- 24 to 48 hours. If we are at 48h before, it's not ok. |
||||
326 | * |
||||
327 | * @param \DateInterval $diff Interval between now and date to read |
||||
328 | * @param DateTime $current DateTime object for now |
||||
329 | * |
||||
330 | * @return bool |
||||
331 | */ |
||||
332 | protected function humanDateIsYesterdayOrTomorrow( |
||||
333 | \DateInterval $diff, |
||||
334 | \DateTime $current |
||||
335 | ): bool { |
||||
336 | //To check the range from 24h to 48h |
||||
337 | if (($diff->d === 1 && $diff->m === 0 && $diff->y === 0) === false) { |
||||
338 | return false; |
||||
339 | } |
||||
340 | |||||
341 | /** |
||||
342 | * With $diff->d === 1, we know if we are in range from 24h to 48h. |
||||
343 | * But yesterday or tomorrow day can finish into the range. |
||||
344 | * |
||||
345 | * Example : |
||||
346 | * |
||||
347 | * [---03/10---][---04/10---][---05/10---] |
||||
348 | * ---|----|-------|----|-------|----|------ |
||||
349 | * -48h -24h $this |
||||
350 | * [ $diff->d=1 ] |
||||
351 | * |
||||
352 | * Like we can see, the $diff->d=1 is not only on the 04/10, but also |
||||
353 | * on 03/10 because the range is from 24h to 48h before the date. |
||||
354 | * So we need a check to not display "yesterday" for the 03/10. |
||||
355 | */ |
||||
356 | |||||
357 | $twoDays = clone $current; |
||||
358 | if ($diff->invert === 0) { |
||||
359 | $twoDays->modify('-2 days'); |
||||
360 | } else { |
||||
361 | $twoDays->modify('+2 days'); |
||||
362 | } |
||||
363 | |||||
364 | if ($this->format('d') === $twoDays->format('d')) { |
||||
365 | return false; |
||||
366 | } |
||||
367 | |||||
368 | return true; |
||||
369 | } |
||||
370 | |||||
371 | /** |
||||
372 | * Format date to human readable when the date is now |
||||
373 | * |
||||
374 | * @param object $parsedTxt Texts returned by humanReadable method |
||||
375 | * |
||||
376 | * @return void |
||||
377 | */ |
||||
378 | protected function humanDateNow($parsedTxt) |
||||
379 | { |
||||
380 | $currentClass = get_called_class(); |
||||
381 | $parsedTxt->date = $currentClass::$humanReadableI18n['now']; |
||||
0 ignored issues
–
show
|
|||||
382 | } |
||||
383 | |||||
384 | /** |
||||
385 | * Format date to human readable when date is today |
||||
386 | * |
||||
387 | * @param object $parsedTxt Texts returned by humanReadable method |
||||
388 | * @param \DateInterval $diff Interval between now and date to read |
||||
389 | * |
||||
390 | * @return void |
||||
391 | */ |
||||
392 | protected function humanDateToday($parsedTxt, \DateInterval $diff) |
||||
393 | { |
||||
394 | $textKey = 'today_past'; |
||||
395 | if ($diff->invert === 1) { |
||||
396 | $textKey = 'today_future'; |
||||
397 | } |
||||
398 | |||||
399 | $time = ''; |
||||
400 | if ($diff->h === 0 && $diff->i === 0) { |
||||
401 | $time .= $diff->s.'s'; |
||||
402 | } elseif ($diff->h === 0) { |
||||
403 | $time .= $diff->i.'min'; |
||||
404 | } else { |
||||
405 | $time .= $diff->h.'h'; |
||||
406 | } |
||||
407 | |||||
408 | $currentClass = get_called_class(); |
||||
409 | $parsedTxt->date = $currentClass::$humanReadableI18n[$textKey]; |
||||
0 ignored issues
–
show
|
|||||
410 | |||||
411 | $this->humanParseDateAndTimeText($parsedTxt, '', $time); |
||||
412 | } |
||||
413 | |||||
414 | /** |
||||
415 | * Format date to human readable when date is yesterday |
||||
416 | * |
||||
417 | * @param object $parsedTxt Texts returned by humanReadable method |
||||
418 | * |
||||
419 | * @return void |
||||
420 | */ |
||||
421 | protected function humanDateYesterday($parsedTxt) |
||||
422 | { |
||||
423 | $currentClass = get_called_class(); |
||||
424 | $parsedTxt->date = $currentClass::$humanReadableI18n['yesterday']; |
||||
0 ignored issues
–
show
|
|||||
425 | $parsedTxt->time = $currentClass::$humanReadableI18n['time_part']; |
||||
426 | |||||
427 | $time = $this->format($currentClass::$humanReadableFormats['time']); |
||||
0 ignored issues
–
show
|
|||||
428 | |||||
429 | $this->humanParseDateAndTimeText($parsedTxt, '', $time); |
||||
430 | } |
||||
431 | |||||
432 | /** |
||||
433 | * Format date to human readable when date is tomorrow |
||||
434 | * |
||||
435 | * @param object $parsedTxt Texts returned by humanReadable method |
||||
436 | * |
||||
437 | * @return void |
||||
438 | */ |
||||
439 | protected function humanDateTomorrow($parsedTxt) |
||||
440 | { |
||||
441 | $currentClass = get_called_class(); |
||||
442 | $parsedTxt->date = $currentClass::$humanReadableI18n['tomorrow']; |
||||
0 ignored issues
–
show
|
|||||
443 | $parsedTxt->time = $currentClass::$humanReadableI18n['time_part']; |
||||
444 | |||||
445 | $time = $this->format($currentClass::$humanReadableFormats['time']); |
||||
0 ignored issues
–
show
|
|||||
446 | |||||
447 | $this->humanParseDateAndTimeText($parsedTxt, '', $time); |
||||
448 | } |
||||
449 | |||||
450 | /** |
||||
451 | * Format date to human readable when date is not now, today or yesterday |
||||
452 | * |
||||
453 | * @param object $parsedTxt Texts returned by humanReadable method |
||||
454 | * @param \DateTime $current DateTime object for now |
||||
455 | * |
||||
456 | * @return void |
||||
457 | */ |
||||
458 | protected function humanDateOther($parsedTxt, \DateTime $current) |
||||
459 | { |
||||
460 | $currentClass = get_called_class(); |
||||
461 | |||||
462 | $dateFormat = $currentClass::$humanReadableFormats['dateDifferentYear']; |
||||
0 ignored issues
–
show
|
|||||
463 | if ($current->format('Y') === $this->format('Y')) { |
||||
464 | $dateFormat = $currentClass::$humanReadableFormats['dateSameYear']; |
||||
465 | } |
||||
466 | |||||
467 | $parsedTxt->date = $currentClass::$humanReadableI18n['others']; |
||||
0 ignored issues
–
show
|
|||||
468 | $parsedTxt->time = $currentClass::$humanReadableI18n['time_part']; |
||||
469 | |||||
470 | $date = $this->format($dateFormat); |
||||
471 | $time = $this->format($currentClass::$humanReadableFormats['time']); |
||||
472 | |||||
473 | $this->humanParseDateAndTimeText($parsedTxt, $date, $time); |
||||
474 | } |
||||
475 | |||||
476 | /** |
||||
477 | * Replace the expression "{date}" by the $date value and the expression |
||||
478 | * "{time}" by the $time value into properties "date" and "time" of the |
||||
479 | * $parsedTxt object. |
||||
480 | * |
||||
481 | * @param object $parsedTxt Texts returned by humanReadable method |
||||
482 | * @param string $date The date value used to replace "{date}" into texts |
||||
483 | * @param string $time The time value used to replace "{time}" into texts |
||||
484 | * |
||||
485 | * @return void |
||||
486 | */ |
||||
487 | protected function humanParseDateAndTimeText( |
||||
488 | $parsedTxt, |
||||
489 | string $date, |
||||
490 | string $time |
||||
491 | ) { |
||||
492 | $parsedTxt->date = str_replace('{date}', $date, $parsedTxt->date); |
||||
493 | $parsedTxt->date = str_replace('{time}', $time, $parsedTxt->date); |
||||
494 | |||||
495 | $parsedTxt->time = str_replace('{date}', $date, $parsedTxt->time); |
||||
496 | $parsedTxt->time = str_replace('{time}', $time, $parsedTxt->time); |
||||
497 | } |
||||
498 | } |
||||
499 |