From 51247523af88f8047aee6ca41b2dbdfeb101d60e Mon Sep 17 00:00:00 2001
From: Owen Bush <ojb@ukhhf.co.uk>
Date: Fri, 22 Mar 2019 20:14:23 -0600
Subject: [PATCH] Create instances for events

---
 recurring_events.api.php      |  11 +
 recurring_events.services.yml |   2 +-
 src/EventCreationService.php  | 383 +++++++++++++++++++++++++++++++++-
 src/Form/EventSeriesForm.php  |   1 -
 4 files changed, 394 insertions(+), 3 deletions(-)

diff --git a/recurring_events.api.php b/recurring_events.api.php
index 1fc99ce3..36d285d2 100644
--- a/recurring_events.api.php
+++ b/recurring_events.api.php
@@ -48,3 +48,14 @@ function hook_recurring_events_month_days_alter(array &$month_days = []) {
   // No events can take place on the 17th of a month.
   unset($month_days[17]);
 }
+
+/**
+ * Alter the event instance entity prior to saving it when creating a series.
+ *
+ * @param array $event_instance
+ *   An array of data to be stored against a event instance.
+ */
+function hook_recurring_events_event_instance_alter(array &$event_instance = []) {
+  // Change the series ID.
+  $event_instance['event_series_id'] = 12;
+}
diff --git a/recurring_events.services.yml b/recurring_events.services.yml
index bbca6544..ff91bf53 100644
--- a/recurring_events.services.yml
+++ b/recurring_events.services.yml
@@ -1,5 +1,5 @@
 services:
   recurring_events.event_creation_service:
     class: Drupal\recurring_events\EventCreationService
-    arguments: ['@string_translation', '@database', '@logger.factory']
+    arguments: ['@string_translation', '@database', '@logger.factory', '@messenger', '@date.formatter']
     
\ No newline at end of file
diff --git a/src/EventCreationService.php b/src/EventCreationService.php
index ec5e7d45..37981eed 100644
--- a/src/EventCreationService.php
+++ b/src/EventCreationService.php
@@ -390,7 +390,7 @@ class EventCreationService {
    * @param Drupal\Core\Form\FormStateInterface $form_state
    *   The form state of an updated event series entity.
    */
-  public static function saveEvent(EventSeries $event, FormStateInterface $form_state) {
+  public function saveEvent(EventSeries $event, FormStateInterface $form_state) {
     // We only need a revision if this is an existing entity.
     if ($event->isNew()) {
       $create_instances = TRUE;
@@ -421,4 +421,385 @@ class EventCreationService {
     }
   }
 
+  /**
+   * Create the event instances from the form state.
+   *
+   * @param Drupal\recurring_events\Entity\EventSeries $event
+   *   The stored event series entity.
+   * @param Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state of an updated event series entity.
+   */
+  public function createInstances(EventSeries $event, FormStateInterface $form_state) {
+    $form_data = $this->convertFormConfigToArray($form_state);
+    $event_instances = [];
+
+    $timezone = new \DateTimeZone(drupal_get_user_timezone());
+    $utc_timezone = new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE);
+
+    if (!empty($form_data['type'])) {
+      switch ($form_data['type']) {
+        case 'custom':
+          if (!empty($form_data['custom_dates'])) {
+            foreach ($form_data['custom_dates'] as $date_range) {
+              // Create this event instance.
+              $event_instances[] = $this->createEventInstance($event, $date_range['start_date'], $date_range['end_date']);
+            }
+          }
+          break;
+
+        case 'weekly':
+          if (!empty($form_data['days'])) {
+            $dates = [];
+
+            // Loop through each weekday and find occurrences of it in the
+            // date range provided.
+            foreach ($form_data['days'] as $weekday) {
+              $weekday_dates = $this->findWeekdaysBetweenDates($weekday, $form_data['start_date'], $form_data['end_date']);
+              $dates = array_merge($dates, $weekday_dates);
+            }
+            $time_parts = $this->convertTimeTo24hourFormat($form_data['time']);
+
+            if (!empty($dates)) {
+              foreach ($dates as $weekly_date) {
+                // Set the time of the start date to be the hours and minutes.
+                $weekly_date->setTime($time_parts[0], $time_parts[1]);
+                // Configure the right timezone.
+                $weekly_date->setTimezone($utc_timezone);
+                // Create a clone of this date.
+                $weekly_date_end = clone $weekly_date;
+                // Add the number of seconds specified in the duration field.
+                $weekly_date_end->modify('+' . $form_data['duration'] . ' seconds');
+                // Create this event instance.
+                $event_instances[] = $this->createEventInstance($event, $weekly_date, $weekly_date_end);
+              }
+            }
+          }
+          break;
+
+        case 'monthly':
+          $dates = [];
+          $time_parts = $this->convertTimeTo24hourFormat($form_data['time']);
+
+          if (!empty($form_data['monthly_type'])) {
+            $dates = [];
+            switch ($form_data['monthly_type']) {
+              case 'weekday':
+                // Loop through each weekday occurrence and weekday.
+                if (!empty($form_data['day_occurrence']) && !empty($form_data['days'])) {
+                  foreach ($form_data['day_occurrence'] as $occurrence) {
+                    foreach ($form_data['days'] as $weekday) {
+                      // Find the occurrence of the specific weekdays within
+                      // each month.
+                      $day_occurrences = $this->findWeekdayOccurrencesBetweenDates($occurrence, $weekday, $form_data['start_date'], $form_data['end_date']);
+                      $dates = array_merge($dates, $day_occurrences);
+                    }
+                  }
+                }
+                break;
+
+              case 'monthday':
+                foreach ($form_data['day_of_month'] as $day_of_month) {
+                  $days_of_month = $this->findMonthDaysBetweenDates($day_of_month, $form_data['start_date'], $form_data['end_date']);
+                  $dates = array_merge($dates, $days_of_month);
+                }
+                break;
+
+            }
+
+            // If valid recurring dates were found.
+            if (!empty($dates)) {
+              foreach ($dates as $monthly_date) {
+                // Set the time of the start date to be the hours and
+                // minutes.
+                $monthly_date->setTime($time_parts[0], $time_parts[1]);
+                // Configure the timezone.
+                $monthly_date->setTimezone($utc_timezone);
+                // Create a clone of this date.
+                $monthly_date_end = clone $monthly_date;
+                // Add the number of seconds specified in the duration
+                // field.
+                $monthly_date_end->modify('+' . $form_data['duration'] . ' seconds');
+                // Create this event instance.
+                $event_instances[] = $this->createEventInstance($event, $monthly_date, $monthly_date_end);
+              }
+            }
+          }
+          break;
+
+      }
+    }
+
+    // Create a message to indicate how many instances were changed.
+    $this->messenger->addMessage($this->translation->translate('A total of @items event instances were created as part of this event series.', [
+      '@items' => count($event_instances),
+    ]));
+    $event->set('event_instances', $event_instances);
+  }
+
+  /**
+   * Convert a time from 12 hour format to 24 hour format.
+   *
+   * @var string $time
+   *   The time to convert to 24 hour format.
+   *
+   * @return array
+   *   An array of time parts.
+   */
+  public function convertTimeTo24hourFormat($time) {
+    $time_parts = [];
+
+    // Split the start time up to separate out hours and minutes.
+    $time_parts = explode(':', $time);
+    // If this is PM then add 12 hours to the hours, unless the time was
+    // set as noon.
+    if (strpos($time_parts[1], 'pm') !== FALSE && $time_parts[0] != '12') {
+      $time_parts[0] += 12;
+    }
+    // If this is AM and the time was midnight, set hours to 00.
+    elseif (strpos($time_parts[1], 'am') !== FALSE && $time_parts[0] == '12') {
+      $time_parts[0] = '00';
+    }
+    // Strip out AM or PM from the time.
+    $time_parts[1] = substr($time_parts[1], 0, -3);
+
+    return $time_parts;
+  }
+
+  /**
+   * Find all the weekday occurrences between two dates.
+   *
+   * @param string $weekday
+   *   The name of the day of the week.
+   * @param Drupal\Core\Datetime\DrupalDateTime $start_date
+   *   The start date.
+   * @param Drupal\Core\Datetime\DrupalDateTime $end_date
+   *   The end date.
+   *
+   * @return array
+   *   An array of matching dates.
+   */
+  public static function findWeekdaysBetweenDates($weekday, DrupalDateTime $start_date, DrupalDateTime $end_date) {
+    $dates = [];
+
+    // Clone the date as we do not want to make changes to the original object.
+    $start = clone $start_date;
+    $end = clone $end_date;
+
+    // We want to create events up to and including the last day, so modify the
+    // end date to be midnight of the next day.
+    $end->modify('midnight next day');
+
+    // If the start date is after the end date then we have an invalid range so
+    // just return nothing.
+    if ($start > $end) {
+      return $dates;
+    }
+
+    // If the start date is not the weekday we are seeking, jump to the next
+    // instance of that weekday.
+    if ($start->format('l') != ucwords($weekday)) {
+      $start->modify('next ' . $weekday);
+    }
+
+    // Loop through a week at a time, storing the date in the array to return
+    // until the end date is surpassed.
+    while ($start <= $end) {
+      // If we do not clone here we end up modifying the value of start in
+      // the array and get some funky dates returned.
+      $dates[] = clone $start;
+      $start->modify('+1 week');
+    }
+
+    return $dates;
+  }
+
+  /**
+   * Find all the day-of-month occurrences between two dates.
+   *
+   * @param int $day_of_month
+   *   The day of the month.
+   * @param Drupal\Core\Datetime\DrupalDateTime $start_date
+   *   The start date.
+   * @param Drupal\Core\Datetime\DrupalDateTime $end_date
+   *   The end date.
+   *
+   * @return array
+   *   An array of matching dates.
+   */
+  public function findMonthDaysBetweenDates($day_of_month, DrupalDateTime $start_date, DrupalDateTime $end_date) {
+    $dates = [];
+
+    // Clone the date as we do not want to make changes to the original object.
+    $start = clone $start_date;
+    $end = clone $end_date;
+
+    // We want to create events up to and including the last day, so modify the
+    // end date to be midnight of the next day.
+    $end->modify('midnight next day');
+
+    // If the start date is after the end date then we have an invalid range so
+    // just return nothing.
+    if ($start > $end) {
+      return $dates;
+    }
+
+    $day_to_check = $day_of_month;
+
+    // If day of month is set to -1 that is the last day of the month, we need
+    // to calculate how many days a month has.
+    if ($day_of_month === '-1') {
+      $day_to_check = $start->format('t');
+    }
+
+    // If the day of the month is after the start date.
+    if ($start->format('d') < $day_to_check) {
+      $new_date = clone $start;
+      $curr_month = $new_date->format('m');
+      $curr_year = $new_date->format('Y');
+
+      // Check to see if that date is a valid date.
+      if (!checkdate($curr_month, $day_to_check, $curr_year)) {
+        // If not, go find the next valid date.
+        $start = $this->findNextMonthDay($day_of_month, $start);
+      }
+      else {
+        // This is a valid date, so let us start there.
+        $start->setDate($curr_year, $curr_month, $day_to_check);
+      }
+    }
+    // If the day of the month is in the past.
+    elseif ($start->format('d') > $day_to_check) {
+      // Find the next valid start date.
+      $start = $this->findNextMonthDay($day_of_month, $start);
+    }
+
+    // Loop through each month checking to see if the day of the month is a
+    // valid day, until the end date has been surpassed.
+    while ($start <= $end) {
+      // If we do not clone here we end up modifying the value of start in
+      // the array and get some funky dates returned.
+      $dates[] = clone $start;
+      // Find the next valid event date.
+      $start = $this->findNextMonthDay($day_of_month, $start);
+    }
+
+    return $dates;
+  }
+
+  /**
+   * Find the next day-of-month occurrence.
+   *
+   * @param int $day_of_month
+   *   The day of the month.
+   * @param Drupal\Core\Datetime\DrupalDateTime $date
+   *   The start date.
+   *
+   * @return Drupal\Core\Datetime\DrupalDateTime
+   *   The next occurrence of a specific day of the month.
+   */
+  public function findNextMonthDay($day_of_month, DrupalDateTime $date) {
+    $new_date = clone $date;
+
+    $curr_month = $new_date->format('m');
+    $curr_year = $new_date->format('Y');
+    $next_month = $curr_month;
+    $next_year = $curr_year;
+
+    do {
+      $next_month = ($next_month + 1) % 12 ?: 12;
+      $next_year = $next_month == 1 ? $next_year + 1 : $next_year;
+
+      // If the desired day of the month is the last day, calculate what that
+      // day is.
+      if ($day_of_month === '-1') {
+        $new_date->setDate($next_year, $next_month, '1');
+        $day_of_month = $new_date->format('t');
+      }
+    } while (checkdate($next_month, $day_of_month, $next_year) === FALSE);
+
+    $new_date->setDate($next_year, $next_month, $day_of_month);
+    return $new_date;
+  }
+
+  /**
+   * Find all the monthly occurrences of a specific weekday between two dates.
+   *
+   * @param string $occurrence
+   *   Which occurrence of the weekday to find.
+   * @param string $weekday
+   *   The name of the day of the week.
+   * @param Drupal\Core\Datetime\DrupalDateTime $start_date
+   *   The start date.
+   * @param Drupal\Core\Datetime\DrupalDateTime $end_date
+   *   The end date.
+   *
+   * @return array
+   *   An array of matching dates.
+   */
+  public function findWeekdayOccurrencesBetweenDates($occurrence, $weekday, DrupalDateTime $start_date, DrupalDateTime $end_date) {
+    $dates = [];
+
+    // Clone the date as we do not want to make changes to the original object.
+    $start = clone $start_date;
+
+    // If the start date is after the end date then we have an invalid range so
+    // just return nothing.
+    if ($start > $end_date) {
+      return $dates;
+    }
+
+    // Grab the occurrence of the weekday we want for this current month.
+    $start->modify($occurrence . ' ' . $weekday . ' of this month');
+
+    // Make sure we didn't just go back in time.
+    if ($start < $start_date) {
+      // Go straight to next month.
+      $start->modify($occurrence . ' ' . $weekday . ' of next month');
+    }
+
+    // Loop through a week at a time, storing the date in the array to return
+    // until the end date is surpassed.
+    while ($start <= $end_date) {
+      // If we do not clone here we end up modifying the value of start in
+      // the array and get some funky dates returned.
+      $dates[] = clone $start;
+      $start->modify($occurrence . ' ' . $weekday . ' of next month');
+    }
+
+    return $dates;
+  }
+
+  /**
+   * Create an event instance from an event series.
+   *
+   * @param Drupal\recurring_events\Entity\EventSeries $event
+   *   The stored event series entity.
+   * @param Drupal\Core\Datetime\DrupalDateTime $start_date
+   *   The start date and time of the event.
+   * @param Drupal\Core\Datetime\DrupalDateTime $end_date
+   *   The end date and time of the event.
+   *
+   * @return static
+   *   The created event instance entity object.
+   */
+  public function createEventInstance(EventSeries $event, DrupalDateTime $start_date, DrupalDateTime $end_date) {
+    $data = [
+      'event_series_id' => $event->id(),
+      'date' => [
+        'value' => $start_date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT),
+        'end_value' => $end_date->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT),
+      ],
+    ];
+
+    \Drupal::moduleHandler()->alter('recurring_events_event_instance', $data);
+
+    $entity = \Drupal::entityTypeManager()
+      ->getStorage('eventinstance')
+      ->create($data);
+
+    $entity->save();
+
+    return $entity;
+  }
+
 }
diff --git a/src/Form/EventSeriesForm.php b/src/Form/EventSeriesForm.php
index 2b6b607d..c829c90c 100644
--- a/src/Form/EventSeriesForm.php
+++ b/src/Form/EventSeriesForm.php
@@ -107,7 +107,6 @@ class EventSeriesForm extends ContentEntityForm {
     ];
 
     if ($editing) {
-      $form['#entity_builders'][] = '::updateRecurringDates';
       if ($this->step === 1) {
         $diff_array = $this->creationService->buildDiffArray($original, $form_state);
 
-- 
GitLab