timeZone)) { $timeZone = new DateTimeZone('UTC'); } $this->timeZone = $timeZone; if (is_array($input)) { $events = $input; } elseif ($input instanceof VEvent) { // Single instance mode. $events = array($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->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( array( '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 DateTime */ public function current() { if ($this->currentDate) { return clone $this->currentDate; } } /** * This method returns the start date for the current iteration of the * event. * * @return DateTime */ public function getDtStart() { if ($this->currentDate) { return clone $this->currentDate; } } /** * This method returns the end date for the current iteration of the * event. * * @return DateTime */ public function getDtEnd() { if (!$this->valid()) { return null; } $end = clone $this->currentDate; $end->modify('+' . $this->eventDuration . ' seconds'); return $end; } /** * Returns a VEVENT for the current iterations of the event. * * This VEVENT will have a recurrence id, and it's 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()); } // Including a RECURRENCE-ID to the object, unless this is the first // object. // // The inner recurIterator is always one step ahead, this is why we're // checking for the key being higher than 1. if ($this->recurIterator->key() > 1) { $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() { return !!$this->currentDate; } /** * Sets the iterator back to the starting point. */ public function rewind() { $this->recurIterator->rewind(); // re-creating overridden event index. $index = array(); 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. * * @return void */ 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) { $offset = end($this->overriddenEventsIndex); $timestamp = key($this->overriddenEventsIndex); 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); // Exit point! return; } } $this->currentDate = $nextDate; } /** * Quickly jump to a date in the future. * * @param DateTime $dateTime */ public function fastForward(DateTime $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 = array(); /** * Overridden event index. * * Key is timestamp, value is the index 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 = array(); /** * Internal event counter * * @var int */ protected $counter; /** * The very start of the iteration process. * * @var DateTime */ protected $startDate; /** * Where we are currently in the iteration process * * @var DateTime */ 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 DateTime */ protected $nextDate; }