timeZone = $timeZone; if (is_array($input)) { $events = $input; } elseif ($input instanceof VEvent) { // Single instance mode. $events = [$input]; } else { // Calendar + UID mode. $uid = (string) $uid; if (!$uid) { throw new InvalidArgumentException('The UID argument is required when a VCALENDAR is passed to this constructor'); } if (!isset($input->VEVENT)) { throw new InvalidArgumentException('No events found in this calendar'); } $events = $input->getByUID($uid); } foreach ($events as $vevent) { if (!isset($vevent->{'RECURRENCE-ID'})) { $this->masterEvent = $vevent; } else { $this->exceptions[ $vevent->{'RECURRENCE-ID'}->getDateTime($this->timeZone)->getTimeStamp() ] = true; $this->overriddenEvents[] = $vevent; } } if (!$this->masterEvent) { // No base event was found. CalDAV does allow cases where only // overridden instances are stored. // // In this particular case, we're just going to grab the first // event and use that instead. This may not always give the // desired result. if (!count($this->overriddenEvents)) { throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: '.$uid); } $this->masterEvent = array_shift($this->overriddenEvents); } $this->startDate = $this->masterEvent->DTSTART->getDateTime($this->timeZone); $this->allDay = !$this->masterEvent->DTSTART->hasTime(); if (isset($this->masterEvent->EXDATE)) { foreach ($this->masterEvent->EXDATE as $exDate) { foreach ($exDate->getDateTimes($this->timeZone) as $dt) { $this->exceptions[$dt->getTimeStamp()] = true; } } } if (isset($this->masterEvent->DTEND)) { $this->eventDuration = $this->masterEvent->DTEND->getDateTime($this->timeZone)->getTimeStamp() - $this->startDate->getTimeStamp(); } elseif (isset($this->masterEvent->DURATION)) { $duration = $this->masterEvent->DURATION->getDateInterval(); $end = clone $this->startDate; $end = $end->add($duration); $this->eventDuration = $end->getTimeStamp() - $this->startDate->getTimeStamp(); } elseif ($this->allDay) { $this->eventDuration = 3600 * 24; } else { $this->eventDuration = 0; } if (isset($this->masterEvent->RDATE)) { $this->recurIterator = new RDateIterator( $this->masterEvent->RDATE->getParts(), $this->startDate ); } elseif (isset($this->masterEvent->RRULE)) { $this->recurIterator = new RRuleIterator( $this->masterEvent->RRULE->getParts(), $this->startDate ); } else { $this->recurIterator = new RRuleIterator( [ 'FREQ' => 'DAILY', 'COUNT' => 1, ], $this->startDate ); } $this->rewind(); if (!$this->valid()) { throw new NoInstancesException('This recurrence rule does not generate any valid instances'); } } /** * Returns the date for the current position of the iterator. * * @return DateTimeImmutable */ public function current() { if ($this->currentDate) { return clone $this->currentDate; } } /** * This method returns the start date for the current iteration of the * event. * * @return DateTimeImmutable */ public function getDtStart() { if ($this->currentDate) { return clone $this->currentDate; } } /** * This method returns the end date for the current iteration of the * event. * * @return DateTimeImmutable */ public function getDtEnd() { if (!$this->valid()) { return; } $end = clone $this->currentDate; return $end->modify('+'.$this->eventDuration.' seconds'); } /** * Returns a VEVENT for the current iterations of the event. * * This VEVENT will have a recurrence id, and its DTSTART and DTEND * altered. * * @return VEvent */ public function getEventObject() { if ($this->currentOverriddenEvent) { return $this->currentOverriddenEvent; } $event = clone $this->masterEvent; // Ignoring the following block, because PHPUnit's code coverage // ignores most of these lines, and this messes with our stats. // // @codeCoverageIgnoreStart unset( $event->RRULE, $event->EXDATE, $event->RDATE, $event->EXRULE, $event->{'RECURRENCE-ID'} ); // @codeCoverageIgnoreEnd $event->DTSTART->setDateTime($this->getDtStart(), $event->DTSTART->isFloating()); if (isset($event->DTEND)) { $event->DTEND->setDateTime($this->getDtEnd(), $event->DTEND->isFloating()); } $recurid = clone $event->DTSTART; $recurid->name = 'RECURRENCE-ID'; $event->add($recurid); return $event; } /** * Returns the current position of the iterator. * * This is for us simply a 0-based index. * * @return int */ public function key() { // The counter is always 1 ahead. return $this->counter - 1; } /** * This is called after next, to see if the iterator is still at a valid * position, or if it's at the end. * * @return bool */ public function valid() { if ($this->counter > Settings::$maxRecurrences && -1 !== Settings::$maxRecurrences) { throw new MaxInstancesExceededException('Recurring events are only allowed to generate '.Settings::$maxRecurrences); } return (bool) $this->currentDate; } /** * Sets the iterator back to the starting point. */ public function rewind() { $this->recurIterator->rewind(); // re-creating overridden event index. $index = []; foreach ($this->overriddenEvents as $key => $event) { $stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp(); $index[$stamp][] = $key; } krsort($index); $this->counter = 0; $this->overriddenEventsIndex = $index; $this->currentOverriddenEvent = null; $this->nextDate = null; $this->currentDate = clone $this->startDate; $this->next(); } /** * Advances the iterator with one step. */ public function next() { $this->currentOverriddenEvent = null; ++$this->counter; if ($this->nextDate) { // We had a stored value. $nextDate = $this->nextDate; $this->nextDate = null; } else { // We need to ask rruleparser for the next date. // We need to do this until we find a date that's not in the // exception list. do { if (!$this->recurIterator->valid()) { $nextDate = null; break; } $nextDate = $this->recurIterator->current(); $this->recurIterator->next(); } while (isset($this->exceptions[$nextDate->getTimeStamp()])); } // $nextDate now contains what rrule thinks is the next one, but an // overridden event may cut ahead. if ($this->overriddenEventsIndex) { $offsets = end($this->overriddenEventsIndex); $timestamp = key($this->overriddenEventsIndex); $offset = end($offsets); if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) { // Overridden event comes first. $this->currentOverriddenEvent = $this->overriddenEvents[$offset]; // Putting the rrule next date aside. $this->nextDate = $nextDate; $this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone); // Ensuring that this item will only be used once. array_pop($this->overriddenEventsIndex[$timestamp]); if (!$this->overriddenEventsIndex[$timestamp]) { array_pop($this->overriddenEventsIndex); } // Exit point! return; } } $this->currentDate = $nextDate; } /** * Quickly jump to a date in the future. * * @param DateTimeInterface $dateTime */ public function fastForward(DateTimeInterface $dateTime) { while ($this->valid() && $this->getDtEnd() <= $dateTime) { $this->next(); } } /** * Returns true if this recurring event never ends. * * @return bool */ public function isInfinite() { return $this->recurIterator->isInfinite(); } /** * RRULE parser. * * @var RRuleIterator */ protected $recurIterator; /** * The duration, in seconds, of the master event. * * We use this to calculate the DTEND for subsequent events. */ protected $eventDuration; /** * A reference to the main (master) event. * * @var VEVENT */ protected $masterEvent; /** * List of overridden events. * * @var array */ protected $overriddenEvents = []; /** * Overridden event index. * * Key is timestamp, value is the list of indexes of the item in the $overriddenEvent * property. * * @var array */ protected $overriddenEventsIndex; /** * A list of recurrence-id's that are either part of EXDATE, or are * overridden. * * @var array */ protected $exceptions = []; /** * Internal event counter. * * @var int */ protected $counter; /** * The very start of the iteration process. * * @var DateTimeImmutable */ protected $startDate; /** * Where we are currently in the iteration process. * * @var DateTimeImmutable */ protected $currentDate; /** * The next date from the rrule parser. * * Sometimes we need to temporary store the next date, because an * overridden event came before. * * @var DateTimeImmutable */ protected $nextDate; /** * The event that overwrites the current iteration. * * @var VEVENT */ protected $currentOverriddenEvent; }