appName)) { throw new \InvalidArgumentException('App name must be provided for ICS generation.'); } } public function generate(IcsContentDto $event): string { $dateFormat = 'Ymd\THis\Z'; $lines = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//' . $this->appName . '//NONSGML v1.0//EN', 'BEGIN:VEVENT', 'UID:' . $event->uid, 'DTSTAMP:' . now()->setTimezone('UTC')->format($dateFormat), 'DTSTART:' . $event->startsAt->setTimezone('UTC')->format($dateFormat), 'DTEND:' . $event->endsAt->setTimezone('UTC')->format($dateFormat), 'SUMMARY:' . $this->escape($event->summary), 'DESCRIPTION:' . $this->escape($event->description), ]; if ($event->url) { $lines[] = 'URL:' . $this->escape($event->url); } $lines[] = 'END:VEVENT'; $lines[] = 'END:VCALENDAR'; // NOTE: ICS files must have CRLF line endings return implode("\r\n", $lines) . "\r\n"; } private function escape(string $text): string { $text = str_replace('\\', '\\\\', $text); $text = str_replace(';', '\;', $text); $text = str_replace(',', '\,', $text); $text = str_replace("\n", '\n', $text); return $text; } }