From 49a844e93ad679fac34f1788e9e6a8e1c0401cf5 Mon Sep 17 00:00:00 2001
From: Camilo Ernesto Escobar Bedoya <escobar@urbaninsight.com>
Date: Sun, 18 Jun 2023 12:23:13 -0500
Subject: [PATCH] Issue #3362297 Provide new configuration option in the
 Registrant Settings form to enable/disabled the email notifications queue

---
 .../recurring_events_registration.schema.yml  |  3 +
 .../recurring_events_reminders.module         | 16 +++-
 .../recurring_events_registration.api.php     | 33 ++++++++-
 .../recurring_events_registration.module      | 74 +++++++++++++++++--
 .../src/Form/RegistrantSettingsForm.php       | 41 +++++++++-
 .../src/NotificationService.php               | 32 +++++++-
 6 files changed, 182 insertions(+), 17 deletions(-)

diff --git a/modules/recurring_events_registration/config/schema/recurring_events_registration.schema.yml b/modules/recurring_events_registration/config/schema/recurring_events_registration.schema.yml
index 4382c19..5c208e5 100644
--- a/modules/recurring_events_registration/config/schema/recurring_events_registration.schema.yml
+++ b/modules/recurring_events_registration/config/schema/recurring_events_registration.schema.yml
@@ -26,6 +26,9 @@ recurring_events_registration.registrant.config:
     email_notifications:
       type: boolean
       label: 'Whether to enable email notifications'
+    email_notifications_queue:
+      type: boolean
+      label: 'Whether to use the email notifications queue'
     notifications:
       type: sequence
       sequence:
diff --git a/modules/recurring_events_registration/modules/recurring_events_reminders/recurring_events_reminders.module b/modules/recurring_events_registration/modules/recurring_events_reminders/recurring_events_reminders.module
index 3e5b246..7e819f1 100644
--- a/modules/recurring_events_registration/modules/recurring_events_reminders/recurring_events_reminders.module
+++ b/modules/recurring_events_registration/modules/recurring_events_reminders/recurring_events_reminders.module
@@ -140,6 +140,8 @@ function recurring_events_reminders_cron() {
     ->accessCheck(FALSE)
     ->execute();
 
+    print_r(array_keys($event_instances));
+
   if (!empty($event_instances)) {
     $instances = \Drupal::entityTypeManager()->getStorage('eventinstance')->loadMultiple($event_instances);
 
@@ -156,15 +158,23 @@ function recurring_events_reminders_cron() {
       $registrants = $registration_creation_service->retrieveRegisteredParties();
 
       if (empty($registrants)) {
-        return;
+        continue;
       }
 
       $key = 'registration_reminder';
 
+      $config = \Drupal::config('recurring_events_registration.registrant.config');
+      $queue_is_enabled = $config->get('email_notifications_queue');
+
       // Send an email to all registrants.
       foreach ($registrants as $registrant) {
-        // Add each notification to be sent to the queue.
-        \Drupal::service('recurring_events_registration.notification_service')->addEmailNotificationToQueue($key, $registrant);
+        if ($queue_is_enabled) {
+          // Add each notification to be sent to the queue.
+          \Drupal::service('recurring_events_registration.notification_service')->addEmailNotificationToQueue($key, $registrant);
+        }
+        else {
+          recurring_events_registration_send_notification($key, $registrant);
+        }
       }
     }
   }
diff --git a/modules/recurring_events_registration/recurring_events_registration.api.php b/modules/recurring_events_registration/recurring_events_registration.api.php
index 5e0eb4f..802ec4b 100644
--- a/modules/recurring_events_registration/recurring_events_registration.api.php
+++ b/modules/recurring_events_registration/recurring_events_registration.api.php
@@ -31,10 +31,10 @@ function hook_recurring_events_registration_first_waitlist_alter(Registrant $reg
 }
 
 /**
- * Alter whether a notification will be sent based on properties of the Registrant
+ * Alter whether a notification will be sent based on properties of the Registrant.
  *
  * @param bool $send_email
- *   Whether the notification email is sent
+ *   Whether the notification email is sent.
  * @param Drupal\recurring_events_registration\Entity\RegistrantInterface $registrant
  */
 function hook_recurring_events_registration_send_notification_alter(bool &$send_email, RegistrantInterface $registrant) {
@@ -63,3 +63,32 @@ function hook_recurring_events_registration_notification_types_alter(array &$not
     'description' => t('Send an email to registrants when the event name changes?'),
   ];
 }
+
+/**
+ * Alter the `$params` passed to email functions when sending notifications.
+ *
+ * Developers can get the data from `$registrant` entity. The `$params` array
+ * is used later as `$params` in `hook_mail()` and `$message['params']` in
+ * `hook_mail_alter()`.
+ *
+ * We encourage developers to make use of this hook to define any value in the
+ * params that could be necessary to perform any logic in the mail hooks
+ * (ideally scalar values, custom arrays or custom objects. No loaded entities
+ * and no configuration objects (since for queued messages, those could have
+ * changed or been deleted by the moment the queue worker is called).
+ *
+ * @param array $params
+ *   The params array.
+ * @param \Drupal\recurring_events_registration\Entity\RegistrantInterface $registrant
+ *   The Registrant entity. Based on it, developers can perform the logic to
+ *   alter the params array.
+ */
+function hook_recurring_events_registration_message_params_alter(array &$params, RegistrantInterface $registrant) {
+  // Add a new parameter to the params based on some logic over a registrant
+  // field.
+  if ($registrant->hasField('some_field') && !$registrant->get('some_field')->isEmpty()) {
+    $some_field_value = $registrant->get('some_field')->first()->getString();
+    $value = do_something($some_field_value);
+    $params['custom_param'] = $value;
+  }
+}
diff --git a/modules/recurring_events_registration/recurring_events_registration.module b/modules/recurring_events_registration/recurring_events_registration.module
index 3ba086a..1b380d0 100644
--- a/modules/recurring_events_registration/recurring_events_registration.module
+++ b/modules/recurring_events_registration/recurring_events_registration.module
@@ -237,10 +237,18 @@ function recurring_events_registration_recurring_events_save_pre_instances_delet
 
   $key = 'series_modification_notification';
 
+  $config = \Drupal::config('recurring_events_registration.registrant.config');
+  $queue_is_enabled = $config->get('email_notifications_queue');
+
   // Send an email to all registrants.
   foreach ($registrants as $registrant) {
-    // Add each notification to be sent to the queue.
-    \Drupal::service('recurring_events_registration.notification_service')->addEmailNotificationToQueue($key, $registrant);
+    if ($queue_is_enabled) {
+      // Add each notification to be sent to the queue.
+      \Drupal::service('recurring_events_registration.notification_service')->addEmailNotificationToQueue($key, $registrant);
+    }
+    else {
+      recurring_events_registration_send_notification($key, $registrant);
+    }
     $registrant->delete();
   }
 }
@@ -270,10 +278,18 @@ function recurring_events_registration_entity_update(EntityInterface $entity) {
 
       $key = 'instance_modification_notification';
 
+      $config = \Drupal::config('recurring_events_registration.registrant.config');
+      $queue_is_enabled = $config->get('email_notifications_queue');
+
       // Send an email to all registrants.
       foreach ($registrants as $registrant) {
-        // Add each notification to be sent to the queue.
-        \Drupal::service('recurring_events_registration.notification_service')->addEmailNotificationToQueue($key, $registrant);
+        if ($queue_is_enabled) {
+          // Add each notification to be sent to the queue.
+          \Drupal::service('recurring_events_registration.notification_service')->addEmailNotificationToQueue($key, $registrant);
+        }
+        else {
+          recurring_events_registration_send_notification($key, $registrant);
+        }
       }
     }
   }
@@ -293,12 +309,20 @@ function recurring_events_registration_recurring_events_pre_delete_instance(Even
 
   $key = 'instance_deletion_notification';
 
+  $config = \Drupal::config('recurring_events_registration.registrant.config');
+  $queue_is_enabled = $config->get('email_notifications_queue');
+
   // Send an email to all registrants.
   foreach ($registrants as $registrant) {
     // Only send email notifications if this event instance is in the future.
     if ($registration_creation_service->eventInstanceIsInFuture()) {
-      // Add each notification to be sent to the queue.
-      \Drupal::service('recurring_events_registration.notification_service')->addEmailNotificationToQueue($key, $registrant);
+      if ($queue_is_enabled) {
+        // Add each notification to be sent to the queue.
+        \Drupal::service('recurring_events_registration.notification_service')->addEmailNotificationToQueue($key, $registrant);
+      }
+      else {
+        recurring_events_registration_send_notification($key, $registrant);
+      }
     }
     $registrant->delete();
   }
@@ -318,10 +342,18 @@ function recurring_events_registration_recurring_events_pre_delete_instances(Eve
   if (!empty($future_registrants)) {
     $key = 'series_deletion_notification';
 
+    $config = \Drupal::config('recurring_events_registration.registrant.config');
+    $queue_is_enabled = $config->get('email_notifications_queue');
+
     // Send an email to the future registrants.
     foreach ($future_registrants as $registrant) {
-      // Add each notification to be sent to the queue.
-      \Drupal::service('recurring_events_registration.notification_service')->addEmailNotificationToQueue($key, $registrant);
+      if ($queue_is_enabled) {
+        // Add each notification to be sent to the queue.
+        \Drupal::service('recurring_events_registration.notification_service')->addEmailNotificationToQueue($key, $registrant);
+      }
+      else {
+        recurring_events_registration_send_notification($key, $registrant);
+      }
     }
   }
 
@@ -358,6 +390,32 @@ function recurring_events_registration_send_notification($key, RegistrantInterfa
       'registrant' => $registrant,
     ];
 
+    // Allow modules to add data to the `$params`. Developers can get data from
+    // `$registrant`. Those `$params` can be used later as the
+    // `$params` in `hook_mail()` and `$message['params']` in
+    // `hook_mail_alter()`.
+    // Although the `$registrant` itself is already being passed in the
+    // `$params`, which means that `$registrant` can be accessed via
+    // `$params['registrant']` in `hook_mail()` or
+    // `$message['params']['registrant']` in `hook_mail_alter()`, that is only
+    // true for messages that are not queued (i.e. messages that are sent
+    // immediately through this current function). In queued messages, it is
+    // not possible to pass the `$registrant` entity, because the registrant
+    // could not longer exist when the queue worker takes action and sends the
+    // email, so it would be unsafe to try to access it in the mail hooks.
+    // For further explanation:
+    // @see \Drupal\recurring_events_registration\NotificationService::addEmailNotificationToQueue()
+    // We discourage the use of the $registrant entity via
+    // `$params['registrant']` in `hook_mail()` and
+    // `$message['params']['registrant']` in `hook_mail_alter()`, even for
+    // messages that are not queued. For consistency, We encourage developers
+    // to make use of the
+    // `hook_recurring_events_registration_message_params_alter()` to define
+    // any value in the params that could be necessary to perform any logic
+    // in the mail hooks (ideally scalar values, custom arrays or custom
+    // objects.
+    \Drupal::moduleHandler()->alter('recurring_events_registration_message_params', $params, $registrant);
+
     $to = $registrant->email->value;
 
     $mail = \Drupal::service('plugin.manager.mail');
diff --git a/modules/recurring_events_registration/src/Form/RegistrantSettingsForm.php b/modules/recurring_events_registration/src/Form/RegistrantSettingsForm.php
index c493770..ee02d91 100644
--- a/modules/recurring_events_registration/src/Form/RegistrantSettingsForm.php
+++ b/modules/recurring_events_registration/src/Form/RegistrantSettingsForm.php
@@ -123,7 +123,8 @@ class RegistrantSettingsForm extends ConfigFormBase {
       ->set('successfully_updated_waitlist', $form_state->getValue('successfully_updated_waitlist'))
       ->set('already_registered', $form_state->getValue('already_registered'))
       ->set('registration_closed', $form_state->getValue('registration_closed'))
-      ->set('email_notifications', $form_state->getValue('email_notifications'));
+      ->set('email_notifications', $form_state->getValue('email_notifications'))
+      ->set('email_notifications_queue', $form_state->getValue('email_notifications_queue'));
 
     if ($config->getOriginal('use_admin_theme') != $config->get('use_admin_theme')) {
       $this->routeBuilder->setRebuildNeeded();
@@ -313,6 +314,44 @@ class RegistrantSettingsForm extends ConfigFormBase {
       '#default_value' => $config->get('email_notifications'),
     ];
 
+    $form['notifications']['email_notifications_queue'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Send Email Notifications using a queue?'),
+      '#description' => $this->t('Email notifications can be added to a queue to be processed on each cron run. This could be beneficial if you have a large number of registrants to your series and/or instances, to prevent the system from crashing when sending notifications to all of those recipients at once. Depending on your PHP configuration, your system may hit time and memory limits when sending notifications to a massive list of registered people. In particular, the notification types below are designed to send an email to all registrants of a specific series or instance, so those might be the problematic ones:<br>
+        <ul>
+          <li>Instance Deletion Notification</li>
+          <li>Series Deletion Notification</li>
+          <li>Instance Modification Notification</li>
+          <li>Series Modification Notification</li>
+          <li>Registration Reminder</li>
+        </ul>
+        <br>If you check this option, emails corresponding to those notification types will be queued and a queue worker will process as many items as it can in 30 seconds on each cron run.<br><br>
+        How long it takes for the queued notification list to be fully processed depends on three factors:
+        <ol>
+          <li>The number of notifications in the queue</li>
+          <li>How often Drupal cron runs</li>
+          <li>The number of seconds used by the queue worker on each cron run to process the items (it was set to 30)</li>
+        </ol>
+        <br>Notification types that are sent to only one recipient continue to be sent immediately as soon as the trigger action occurs, regardless of this setting, namely:<br>
+        <ul>
+          <li>Registration Notification</li>
+          <li>Waitlist Notification</li>
+          <li>Promotion Notification</li>
+        </ul>
+        <br><b>Important note for developers:</b><br>
+          When the notification types that are not queued (this is always the case for the above-mentioned notification types, which are sent to a single recipient. It will also be the case for all notification types if you uncheck this option) the registrant entity will be passed in the params to <b><em>hook_mail()</em></b> and <b><em>hook_mail_alter()</em></b>. It will be accessible through <b><em>$params[\'registrant\']</em></b> in the first case and <b><em>$message[\'params\'][\'registrant\']</em></b> in the second.<br>
+          However, when notifications are queued it is not possible to pass the registrant entity to mail hooks, since it is likely that the entity no longer exists by the moment the queue worker takes action and sends the email.<br>
+          <b>That is why we highly discourage the use of the registrant entity in mail hooks</b>. To maintain consistency between the two models (queued and non-queued messages), we encourage developers to make use of the <b><em>hook_recurring_events_registration_message_params_alter()</em></b> to define any value in the params that might be needed to perform any logic on the mail hooks.<br>
+          See more detail about that hook in <em>recurring_events_registration.api.php</em>.
+      '),
+      '#default_value' => $config->get('email_notifications_queue'),
+      '#states' => [
+        'visible' => [
+          'input[name="email_notifications"]' => ['checked' => TRUE],
+        ],
+      ],
+    ];
+
     $form['notifications']['emails'] = [
       '#type' => 'vertical_tabs',
       '#title' => $this->t('Emails'),
diff --git a/modules/recurring_events_registration/src/NotificationService.php b/modules/recurring_events_registration/src/NotificationService.php
index 24373e6..58ce692 100644
--- a/modules/recurring_events_registration/src/NotificationService.php
+++ b/modules/recurring_events_registration/src/NotificationService.php
@@ -494,9 +494,35 @@ class NotificationService {
         'body' => $message,
         'from' => $from,
       ];
-      // Allow modules to add data to the `$params`. They can get the data from
-      // `$registrant`. Those `$params` are used later as the
-      // `$message['params']` in mail hooks.
+      // Allow modules to add data to the `$params`. Developers can get data 
+      // from `$registrant`. Those `$params` can be used later as the
+      // `$params` in `hook_mail()` and `$message['params']` in
+      // `hook_mail_alter()`.
+      // In queued messages, we are not passing the `$registrant` entity as a
+      // param (unlike it is being done in non-queued messages
+      // `recurring_events_registration_send_notification`), because the entity
+      // could no longer exist when the queue worker takes action and sends the
+      // email. For example, as mentioned above in another comment, there are
+      // some notification types that require the `$registrant` to be deleted
+      // as part of the same operation that generates the notification, for
+      // example: the notifications corresponding to the keys
+      // 'series_modification_notification' and
+      // 'instance_deletion_notification'.
+      // Therefore, those entities won't be available in the queue worker.
+      // It would be unsafe to try to access a registrant for a queued message
+      // via `$params['registrant']` in `hook_mail()` or
+      // `$message['params']['registrant']` in `hook_mail_alter()`.
+      // We encourage developers to make use of the
+      // `hook_recurring_events_registration_message_params_alter()` to define
+      // any value in the params that could be necessary to perform any logic
+      // in the mail hooks (ideally scalar values, custom arrays or custom
+      // objects. No loaded entities and no configuration objects, since those
+      // could have changed or been deleted by the moment the queue worker is
+      // called), those values will be added to the queued item and will be
+      // available in the queue worker when it processes the item and in the
+      // mail hooks later.
+      // @see recurring_events_registration_recurring_events_save_pre_instances_deletion()
+      // @see recurring_events_registration_recurring_events_pre_delete_instance
       $this->moduleHandler->alter('recurring_events_registration_message_params', $params, $registrant);
       $item->params = $params;
 
-- 
GitLab