add('PRODID', '-//Kithkin//Laravel CalDAV//EN'); $vcalendar->add('VERSION', '2.0'); $vcalendar->add('CALSCALE', 'GREGORIAN'); $vevent = $vcalendar->add('VEVENT', []); $uid = $data['uid']; $startUtc = $data['start_utc']; $endUtc = $data['end_utc']; $tzid = $data['tzid'] ?? null; $vevent->add('UID', $uid); $vevent->add('DTSTAMP', $startUtc->copy()->utc()); if ($tzid) { $startLocal = $startUtc->copy()->tz($tzid); $endLocal = $endUtc->copy()->tz($tzid); $vevent->add('DTSTART', $startLocal, ['TZID' => $tzid]); $vevent->add('DTEND', $endLocal, ['TZID' => $tzid]); } else { $vevent->add('DTSTART', $startUtc->copy()->utc()); $vevent->add('DTEND', $endUtc->copy()->utc()); } if (!empty($data['summary'])) { $vevent->add('SUMMARY', $data['summary']); } if (!empty($data['description'])) { $vevent->add('DESCRIPTION', $data['description']); } if (!empty($data['location'])) { $vevent->add('LOCATION', $data['location']); } $rrule = $data['rrule'] ?? null; if ($rrule) { $vevent->add('RRULE', $rrule); } $exdates = $data['exdate'] ?? []; if (!empty($exdates)) { foreach ($exdates as $ex) { $dt = Carbon::parse($ex, $tzid ?: 'UTC'); if ($tzid) { $vevent->add('EXDATE', $dt, ['TZID' => $tzid]); } else { $vevent->add('EXDATE', $dt->utc()); } } } $rdates = $data['rdate'] ?? []; if (!empty($rdates)) { foreach ($rdates as $r) { $dt = Carbon::parse($r, $tzid ?: 'UTC'); if ($tzid) { $vevent->add('RDATE', $dt, ['TZID' => $tzid]); } else { $vevent->add('RDATE', $dt->utc()); } } } return $vcalendar->serialize(); } /** * Check if a stored event contains recurrence data. */ public function isRecurring(Event $event): bool { $extra = $event->meta?->extra ?? []; if (!empty($extra['rrule'])) { return true; } return Str::contains($event->calendardata ?? '', ['RRULE', 'RDATE', 'EXDATE']); } /** * Expand recurring instances within the requested range. * * Returns an array of ['start' => Carbon, 'end' => Carbon, 'recurrence_id' => string|null] */ public function expand(Event $event, Carbon $rangeStart, Carbon $rangeEnd): array { $vcalendar = $this->readCalendar($event->calendardata); if (!$vcalendar || empty($vcalendar->VEVENT)) { return []; } $vevent = $vcalendar->VEVENT; $uid = (string) $vevent->UID; $startTz = $vevent->DTSTART?->getDateTime()?->getTimezone() ?? new DateTimeZone('UTC'); $iter = new EventIterator($vcalendar, $uid); $iter->fastForward($rangeStart->copy()->setTimezone($startTz)->toDateTime()); $items = []; while ($iter->valid()) { $start = Carbon::instance($iter->getDTStart()); $end = Carbon::instance($iter->getDTEnd()); if ($start->gt($rangeEnd)) { break; } $startUtc = $start->copy()->utc(); $endUtc = $end->copy()->utc(); $items[] = [ 'start' => $startUtc, 'end' => $endUtc, 'recurrence_id' => $startUtc->format('Ymd\\THis\\Z'), ]; $iter->next(); } return $items; } /** * Resolve a single occurrence by its DTSTART. */ public function resolveOccurrence(Event $event, Carbon $occurrenceStart): ?array { $rangeStart = $occurrenceStart->copy()->subDay(); $rangeEnd = $occurrenceStart->copy()->addDay(); foreach ($this->expand($event, $rangeStart, $rangeEnd) as $occ) { if ($occ['start']->equalTo($occurrenceStart)) { return $occ; } } return null; } public function extractRrule(Event $event): ?string { $vcalendar = $this->readCalendar($event->calendardata); if (!$vcalendar || empty($vcalendar->VEVENT)) { return null; } $vevent = $vcalendar->VEVENT; return isset($vevent->RRULE) ? (string) $vevent->RRULE : null; } private function readCalendar(?string $ical): ?VCalendar { if (!$ical) { return null; } try { return Reader::read($ical); } catch (\Throwable $e) { return null; } } }