Scheduler.php
281 lines
| 1 | <?php |
| 2 | |
| 3 | namespace MailPoet\Newsletter\Scheduler; |
| 4 | |
| 5 | use Carbon\Carbon; |
| 6 | use MailPoet\Logging\Logger; |
| 7 | use MailPoet\Models\Newsletter; |
| 8 | use MailPoet\Models\NewsletterOption; |
| 9 | use MailPoet\Models\NewsletterOptionField; |
| 10 | use MailPoet\Models\NewsletterPost; |
| 11 | use MailPoet\Models\ScheduledTask; |
| 12 | use MailPoet\Models\SendingQueue; |
| 13 | use MailPoet\Tasks\Sending as SendingTask; |
| 14 | use MailPoet\WP\Functions as WPFunctions; |
| 15 | use MailPoet\WP\Posts; |
| 16 | |
| 17 | class Scheduler { |
| 18 | const SECONDS_IN_HOUR = 3600; |
| 19 | const LAST_WEEKDAY_FORMAT = 'L'; |
| 20 | const WORDPRESS_ALL_ROLES = 'mailpoet_all'; |
| 21 | const INTERVAL_IMMEDIATELY = 'immediately'; |
| 22 | const INTERVAL_IMMEDIATE = 'immediate'; |
| 23 | const INTERVAL_DAILY = 'daily'; |
| 24 | const INTERVAL_WEEKLY = 'weekly'; |
| 25 | const INTERVAL_MONTHLY = 'monthly'; |
| 26 | const INTERVAL_NTHWEEKDAY = 'nthWeekDay'; |
| 27 | |
| 28 | static function transitionHook($new_status, $old_status, $post) { |
| 29 | Logger::getLogger('post-notifications')->addInfo( |
| 30 | 'transition post notification hook initiated', |
| 31 | [ |
| 32 | 'post_id' => $post->ID, |
| 33 | 'new_status' => $new_status, |
| 34 | 'old_status' => $old_status, |
| 35 | ] |
| 36 | ); |
| 37 | $types = Posts::getTypes(); |
| 38 | if(($new_status !== 'publish') || !isset($types[$post->post_type])) { |
| 39 | return; |
| 40 | } |
| 41 | self::schedulePostNotification($post->ID); |
| 42 | } |
| 43 | |
| 44 | static function schedulePostNotification($post_id) { |
| 45 | Logger::getLogger('post-notifications')->addInfo( |
| 46 | 'schedule post notification hook', |
| 47 | ['post_id' => $post_id] |
| 48 | ); |
| 49 | $newsletters = self::getNewsletters(Newsletter::TYPE_NOTIFICATION); |
| 50 | if(!count($newsletters)) return false; |
| 51 | foreach($newsletters as $newsletter) { |
| 52 | $post = NewsletterPost::where('newsletter_id', $newsletter->id) |
| 53 | ->where('post_id', $post_id) |
| 54 | ->findOne(); |
| 55 | if($post === false) { |
| 56 | self::createPostNotificationSendingTask($newsletter); |
| 57 | } |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | static function scheduleSubscriberWelcomeNotification($subscriber_id, $segments) { |
| 62 | $newsletters = self::getNewsletters(Newsletter::TYPE_WELCOME); |
| 63 | if(empty($newsletters)) return false; |
| 64 | $result = array(); |
| 65 | foreach($newsletters as $newsletter) { |
| 66 | if($newsletter->event === 'segment' && |
| 67 | in_array($newsletter->segment, $segments) |
| 68 | ) { |
| 69 | $result[] = self::createWelcomeNotificationSendingTask($newsletter, $subscriber_id); |
| 70 | } |
| 71 | } |
| 72 | return $result; |
| 73 | } |
| 74 | |
| 75 | static function scheduleAutomaticEmail($group, $event, $scheduling_condition = false, $subscriber_id = false, $meta = false) { |
| 76 | $newsletters = self::getNewsletters(Newsletter::TYPE_AUTOMATIC, $group); |
| 77 | if(empty($newsletters)) return false; |
| 78 | foreach($newsletters as $newsletter) { |
| 79 | if($newsletter->event !== $event) continue; |
| 80 | if(is_callable($scheduling_condition) && !$scheduling_condition($newsletter)) continue; |
| 81 | self::createAutomaticEmailSendingTask($newsletter, $subscriber_id, $meta); |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | static function scheduleWPUserWelcomeNotification( |
| 86 | $subscriber_id, |
| 87 | $wp_user, |
| 88 | $old_user_data = false |
| 89 | ) { |
| 90 | $newsletters = self::getNewsletters(Newsletter::TYPE_WELCOME); |
| 91 | if(empty($newsletters)) return false; |
| 92 | foreach($newsletters as $newsletter) { |
| 93 | if($newsletter->event === 'user') { |
| 94 | if(!empty($old_user_data['roles'])) { |
| 95 | // do not schedule welcome newsletter if roles have not changed |
| 96 | $old_role = $old_user_data['roles']; |
| 97 | $new_role = $wp_user['roles']; |
| 98 | if($newsletter->role === self::WORDPRESS_ALL_ROLES || |
| 99 | !array_diff($old_role, $new_role) |
| 100 | ) { |
| 101 | continue; |
| 102 | } |
| 103 | } |
| 104 | if($newsletter->role === self::WORDPRESS_ALL_ROLES || |
| 105 | in_array($newsletter->role, $wp_user['roles']) |
| 106 | ) { |
| 107 | self::createWelcomeNotificationSendingTask($newsletter, $subscriber_id); |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | static function createWelcomeNotificationSendingTask($newsletter, $subscriber_id) { |
| 114 | $previously_scheduled_notification = SendingQueue::joinWithSubscribers() |
| 115 | ->where('queues.newsletter_id', $newsletter->id) |
| 116 | ->where('subscribers.subscriber_id', $subscriber_id) |
| 117 | ->findOne(); |
| 118 | if(!empty($previously_scheduled_notification)) return; |
| 119 | $sending_task = SendingTask::create(); |
| 120 | $sending_task->newsletter_id = $newsletter->id; |
| 121 | $sending_task->setSubscribers(array($subscriber_id)); |
| 122 | $sending_task->status = SendingQueue::STATUS_SCHEDULED; |
| 123 | $sending_task->priority = SendingQueue::PRIORITY_HIGH; |
| 124 | $sending_task->scheduled_at = self::getScheduledTimeWithDelay( |
| 125 | $newsletter->afterTimeType, |
| 126 | $newsletter->afterTimeNumber |
| 127 | ); |
| 128 | return $sending_task->save(); |
| 129 | } |
| 130 | |
| 131 | static function createAutomaticEmailSendingTask($newsletter, $subscriber_id, $meta) { |
| 132 | $sending_task = SendingTask::create(); |
| 133 | $sending_task->newsletter_id = $newsletter->id; |
| 134 | if($newsletter->sendTo === 'user' && $subscriber_id) { |
| 135 | $sending_task->setSubscribers(array($subscriber_id)); |
| 136 | } |
| 137 | if($meta) { |
| 138 | $sending_task->__set('meta', $meta); |
| 139 | } |
| 140 | $sending_task->status = SendingQueue::STATUS_SCHEDULED; |
| 141 | $sending_task->priority = SendingQueue::PRIORITY_MEDIUM; |
| 142 | $sending_task->scheduled_at = self::getScheduledTimeWithDelay( |
| 143 | $newsletter->afterTimeType, |
| 144 | $newsletter->afterTimeNumber |
| 145 | ); |
| 146 | return $sending_task->save(); |
| 147 | } |
| 148 | |
| 149 | static function createPostNotificationSendingTask($newsletter) { |
| 150 | $existing_notification_history = Newsletter::table_alias('newsletters') |
| 151 | ->where('newsletters.parent_id', $newsletter->id) |
| 152 | ->where('newsletters.type', Newsletter::TYPE_NOTIFICATION_HISTORY) |
| 153 | ->where('newsletters.status', Newsletter::STATUS_SENDING) |
| 154 | ->join( |
| 155 | MP_SENDING_QUEUES_TABLE, |
| 156 | 'queues.newsletter_id = newsletters.id', |
| 157 | 'queues' |
| 158 | ) |
| 159 | ->join( |
| 160 | MP_SCHEDULED_TASKS_TABLE, |
| 161 | 'queues.task_id = tasks.id', |
| 162 | 'tasks' |
| 163 | ) |
| 164 | ->whereNotEqual('tasks.status', ScheduledTask::STATUS_PAUSED) |
| 165 | ->findOne(); |
| 166 | if($existing_notification_history) { |
| 167 | return; |
| 168 | } |
| 169 | $next_run_date = self::getNextRunDate($newsletter->schedule); |
| 170 | if(!$next_run_date) return; |
| 171 | // do not schedule duplicate queues for the same time |
| 172 | $existing_queue = SendingQueue::findTaskByNewsletterId($newsletter->id) |
| 173 | ->where('tasks.scheduled_at', $next_run_date) |
| 174 | ->findOne(); |
| 175 | if($existing_queue) return; |
| 176 | $sending_task = SendingTask::create(); |
| 177 | $sending_task->newsletter_id = $newsletter->id; |
| 178 | $sending_task->status = SendingQueue::STATUS_SCHEDULED; |
| 179 | $sending_task->scheduled_at = $next_run_date; |
| 180 | $sending_task->save(); |
| 181 | Logger::getLogger('post-notifications')->addInfo( |
| 182 | 'schedule post notification', |
| 183 | ['sending_task' => $sending_task->id(), 'scheduled_at' => $next_run_date] |
| 184 | ); |
| 185 | return $sending_task; |
| 186 | } |
| 187 | |
| 188 | static function processPostNotificationSchedule($newsletter) { |
| 189 | $interval_type = $newsletter->intervalType; |
| 190 | $hour = (int)$newsletter->timeOfDay / self::SECONDS_IN_HOUR; |
| 191 | $week_day = $newsletter->weekDay; |
| 192 | $month_day = $newsletter->monthDay; |
| 193 | $nth_week_day = ($newsletter->nthWeekDay === self::LAST_WEEKDAY_FORMAT) ? |
| 194 | $newsletter->nthWeekDay : |
| 195 | '#' . $newsletter->nthWeekDay; |
| 196 | switch($interval_type) { |
| 197 | case self::INTERVAL_IMMEDIATELY: |
| 198 | $schedule = '* * * * *'; |
| 199 | break; |
| 200 | case self::INTERVAL_IMMEDIATE: |
| 201 | case self::INTERVAL_DAILY: |
| 202 | $schedule = sprintf('0 %s * * *', $hour); |
| 203 | break; |
| 204 | case self::INTERVAL_WEEKLY: |
| 205 | $schedule = sprintf('0 %s * * %s', $hour, $week_day); |
| 206 | break; |
| 207 | case self::INTERVAL_NTHWEEKDAY: |
| 208 | $schedule = sprintf('0 %s ? * %s%s', $hour, $week_day, $nth_week_day); |
| 209 | break; |
| 210 | case self::INTERVAL_MONTHLY: |
| 211 | $schedule = sprintf('0 %s %s * *', $hour, $month_day); |
| 212 | break; |
| 213 | } |
| 214 | $option_field = NewsletterOptionField::where('name', 'schedule')->findOne(); |
| 215 | $relation = NewsletterOption::where('newsletter_id', $newsletter->id) |
| 216 | ->where('option_field_id', $option_field->id) |
| 217 | ->findOne(); |
| 218 | if(!$relation) { |
| 219 | $relation = NewsletterOption::create(); |
| 220 | $relation->newsletter_id = $newsletter->id; |
| 221 | $relation->option_field_id = $option_field->id; |
| 222 | } |
| 223 | $relation->value = $schedule; |
| 224 | $relation->save(); |
| 225 | return $relation->value; |
| 226 | } |
| 227 | |
| 228 | static function getNextRunDate($schedule, $from_timestamp = false) { |
| 229 | $wp = new WPFunctions(); |
| 230 | $from_timestamp = ($from_timestamp) ? $from_timestamp : $wp->currentTime('timestamp'); |
| 231 | try { |
| 232 | $schedule = \Cron\CronExpression::factory($schedule); |
| 233 | $next_run_date = $schedule->getNextRunDate(Carbon::createFromTimestamp($from_timestamp)) |
| 234 | ->format('Y-m-d H:i:s'); |
| 235 | } catch(\Exception $e) { |
| 236 | $next_run_date = false; |
| 237 | } |
| 238 | return $next_run_date; |
| 239 | } |
| 240 | |
| 241 | static function getPreviousRunDate($schedule, $from_timestamp = false) { |
| 242 | $wp = new WPFunctions(); |
| 243 | $from_timestamp = ($from_timestamp) ? $from_timestamp : $wp->currentTime('timestamp'); |
| 244 | try { |
| 245 | $schedule = \Cron\CronExpression::factory($schedule); |
| 246 | $previous_run_date = $schedule->getPreviousRunDate(Carbon::createFromTimestamp($from_timestamp)) |
| 247 | ->format('Y-m-d H:i:s'); |
| 248 | } catch(\Exception $e) { |
| 249 | $previous_run_date = false; |
| 250 | } |
| 251 | return $previous_run_date; |
| 252 | } |
| 253 | |
| 254 | static function getScheduledTimeWithDelay($after_time_type, $after_time_number) { |
| 255 | $wp = new WPFunctions(); |
| 256 | $current_time = Carbon::createFromTimestamp($wp->currentTime('timestamp')); |
| 257 | switch($after_time_type) { |
| 258 | case 'hours': |
| 259 | return $current_time->addHours($after_time_number); |
| 260 | case 'days': |
| 261 | return $current_time->addDays($after_time_number); |
| 262 | case 'weeks': |
| 263 | return $current_time->addWeeks($after_time_number); |
| 264 | default: |
| 265 | return $current_time; |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | static function getNewsletters($type, $group = false) { |
| 270 | return Newsletter::getPublished() |
| 271 | ->filter('filterType', $type, $group) |
| 272 | ->filter('filterStatus', Newsletter::STATUS_ACTIVE) |
| 273 | ->filter('filterWithOptions', $type) |
| 274 | ->findMany(); |
| 275 | } |
| 276 | |
| 277 | static function formatDatetimeString($datetime_string) { |
| 278 | return Carbon::parse($datetime_string)->format('Y-m-d H:i:s'); |
| 279 | } |
| 280 | } |
| 281 |