Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
60 / 60 |
|
100.00% |
4 / 4 |
CRAP | |
100.00% |
1 / 1 |
| MailDateFormatService | |
100.00% |
59 / 59 |
|
100.00% |
4 / 4 |
16 | |
100.00% |
1 / 1 |
| formatRelativeDate | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
7 | |||
| formatOlderDate | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
| formatFullDate | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
| enrichSummary | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | /** @license For full copyright and license information, please see the LICENSE.md file. */ |
| 6 | |
| 7 | namespace App\Modules\Mail\Application\Service; |
| 8 | |
| 9 | defined('AMMONLY_APP') || exit('Direct script access is forbidden.'); |
| 10 | |
| 11 | use App\Modules\Mail\Domain\Model\MailMessageSummaryDto; |
| 12 | use DateTimeImmutable; |
| 13 | use DateTimeZone; |
| 14 | use Throwable; |
| 15 | |
| 16 | /** |
| 17 | * Service for formatting email dates into human-readable relative and localized representations. |
| 18 | * |
| 19 | * @package App\Modules\Mail\Application\Service |
| 20 | */ |
| 21 | final readonly class MailDateFormatService |
| 22 | { |
| 23 | public const string DEFAULT_TIMEZONE = 'Europe/Warsaw'; |
| 24 | |
| 25 | /** |
| 26 | * Formats timestamp into humanized relative time string in English. |
| 27 | * |
| 28 | * @param int $timestamp Unix epoch in seconds. |
| 29 | * @param int|null $now Reference timestamp (defaults to current time). |
| 30 | * @return string Humanized relative time string. |
| 31 | */ |
| 32 | public function formatRelativeDate(int $timestamp, ?int $now = null): string |
| 33 | { |
| 34 | if ($timestamp <= 0) { |
| 35 | return ''; |
| 36 | } |
| 37 | |
| 38 | $now ??= time(); |
| 39 | $diff = $now - $timestamp; |
| 40 | |
| 41 | return match (true) { |
| 42 | $diff < 60 => 'just now', |
| 43 | $diff < 3600 => max(1, (int) floor($diff / 60)) . ' min ago', |
| 44 | $diff < 86400 => (int) floor($diff / 3600) === 1 |
| 45 | ? '1 hour ago' |
| 46 | : (int) floor($diff / 3600) . ' hours ago', |
| 47 | default => $this->formatOlderDate($timestamp, $diff, $now), |
| 48 | }; |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * Formats timestamp older than 24 hours into localized date string. |
| 53 | * |
| 54 | * @param int $timestamp Unix timestamp. |
| 55 | * @param int $diff Seconds elapsed since timestamp. |
| 56 | * @param int $now Current reference timestamp. |
| 57 | * @return string Formatted older date. |
| 58 | */ |
| 59 | private function formatOlderDate(int $timestamp, int $diff, int $now): string |
| 60 | { |
| 61 | $days = (int) floor($diff / 86400); |
| 62 | if ($days === 1) { |
| 63 | return 'yesterday'; |
| 64 | } |
| 65 | if ($days < 7) { |
| 66 | return $days . ' days ago'; |
| 67 | } |
| 68 | |
| 69 | $currentYear = (int) date('Y', $now); |
| 70 | $msgYear = (int) date('Y', $timestamp); |
| 71 | $result = date('Y-m-d', $timestamp); |
| 72 | if ($currentYear === $msgYear) { |
| 73 | $msgDay = (int) date('j', $timestamp); |
| 74 | $monthShort = date('M', $timestamp); |
| 75 | $result = $msgDay . ' ' . $monthShort; |
| 76 | } |
| 77 | |
| 78 | return $result; |
| 79 | } |
| 80 | |
| 81 | /** |
| 82 | * Formats timestamp into precise full date string localized to user timezone. |
| 83 | * |
| 84 | * @param int $timestamp Unix epoch in seconds. |
| 85 | * @param string $timezone IANA timezone identifier. |
| 86 | * @return string Formatted full date string. |
| 87 | */ |
| 88 | public function formatFullDate(int $timestamp, string $timezone = self::DEFAULT_TIMEZONE): string |
| 89 | { |
| 90 | if ($timestamp <= 0) { |
| 91 | return ''; |
| 92 | } |
| 93 | |
| 94 | try { |
| 95 | $tz = new DateTimeZone($timezone); |
| 96 | } catch (Throwable) { |
| 97 | $tz = new DateTimeZone(self::DEFAULT_TIMEZONE); |
| 98 | } |
| 99 | |
| 100 | $dt = (new DateTimeImmutable('@' . $timestamp))->setTimezone($tz); |
| 101 | $day = (int) $dt->format('j'); |
| 102 | $monthName = $dt->format('F'); |
| 103 | $year = $dt->format('Y'); |
| 104 | $time = $dt->format('H:i:s'); |
| 105 | |
| 106 | return sprintf('%d %s %s, %s', $day, $monthName, $year, $time); |
| 107 | } |
| 108 | |
| 109 | /** |
| 110 | * Enriches summary DTO with relative and user-timezone full date strings. |
| 111 | * |
| 112 | * @param MailMessageSummaryDto $summary Summary DTO. |
| 113 | * @param string $timezone User timezone. |
| 114 | * @return MailMessageSummaryDto Enriched DTO copy. |
| 115 | */ |
| 116 | public function enrichSummary( |
| 117 | MailMessageSummaryDto $summary, |
| 118 | string $timezone = self::DEFAULT_TIMEZONE |
| 119 | ): MailMessageSummaryDto { |
| 120 | $timestamp = $summary->dateTimestamp > 0 ? $summary->dateTimestamp : (int) strtotime($summary->date); |
| 121 | $dateRelative = $this->formatRelativeDate($timestamp); |
| 122 | $dateFull = $this->formatFullDate($timestamp, $timezone); |
| 123 | |
| 124 | return new MailMessageSummaryDto( |
| 125 | uid: $summary->uid, |
| 126 | messageId: $summary->messageId, |
| 127 | subject: $summary->subject, |
| 128 | fromName: $summary->fromName, |
| 129 | fromEmail: $summary->fromEmail, |
| 130 | to: $summary->to, |
| 131 | cc: $summary->cc, |
| 132 | date: $summary->date, |
| 133 | dateTimestamp: $timestamp, |
| 134 | size: $summary->size, |
| 135 | isSeen: $summary->isSeen, |
| 136 | isFlagged: $summary->isFlagged, |
| 137 | isAnswered: $summary->isAnswered, |
| 138 | isDraft: $summary->isDraft, |
| 139 | hasAttachments: $summary->hasAttachments, |
| 140 | snippet: $summary->snippet, |
| 141 | folder: $summary->folder, |
| 142 | keywords: $summary->keywords, |
| 143 | dateRelative: $dateRelative, |
| 144 | dateFull: $dateFull |
| 145 | ); |
| 146 | } |
| 147 | } |