Merge pull request #1728 from mailpoet/stats-notifications

Stats notifications [ MAILPOET-1571]
This commit is contained in:
M. Shull
2019-01-28 13:22:21 -05:00
committed by GitHub
26 changed files with 1459 additions and 95 deletions

View File

@@ -1,20 +1,19 @@
<?php
namespace MailPoet\Cron;
use MailPoet\Cron\Workers\Scheduler as SchedulerWorker;
use MailPoet\Cron\Workers\SendingQueue\Migration as MigrationWorker;
use MailPoet\Cron\Workers\SendingQueue\SendingErrorHandler;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Cron\Workers\Bounce as BounceWorker;
use MailPoet\Cron\Workers\KeyCheck\PremiumKeyCheck as PremiumKeyCheckWorker;
use MailPoet\Cron\Workers\KeyCheck\SendingServiceKeyCheck as SendingServiceKeyCheckWorker;
use MailPoet\Cron\Workers\WorkersFactory;
if(!defined('ABSPATH')) exit;
class Daemon {
public $timer;
function __construct() {
/** @var WorkersFactory */
private $workers_factory;
function __construct(WorkersFactory $workers_factory) {
$this->timer = microtime(true);
$this->workers_factory = $workers_factory;
}
function run($settings_daemon_data) {
@@ -22,6 +21,7 @@ class Daemon {
CronHelper::saveDaemon($settings_daemon_data);
try {
$this->executeMigrationWorker();
$this->executeStatsNotificationsWorker();
$this->executeScheduleWorker();
$this->executeQueueWorker();
$this->executeSendingServiceKeyCheckWorker();
@@ -35,32 +35,37 @@ class Daemon {
}
function executeScheduleWorker() {
$scheduler = new SchedulerWorker($this->timer);
$scheduler = $this->workers_factory->createScheduleWorker($this->timer);
return $scheduler->process();
}
function executeQueueWorker() {
$queue = new SendingQueueWorker(new SendingErrorHandler(), $this->timer);
$queue = $this->workers_factory->createQueueWorker($this->timer);
return $queue->process();
}
function executeStatsNotificationsWorker() {
$worker = $this->workers_factory->createStatsNotificationsWorker($this->timer);
return $worker->process();
}
function executeSendingServiceKeyCheckWorker() {
$worker = new SendingServiceKeyCheckWorker($this->timer);
$worker = $this->workers_factory->createSendingServiceKeyCheckWorker($this->timer);
return $worker->process();
}
function executePremiumKeyCheckWorker() {
$worker = new PremiumKeyCheckWorker($this->timer);
$worker = $this->workers_factory->createPremiumKeyCheckWorker($this->timer);
return $worker->process();
}
function executeBounceWorker() {
$bounce = new BounceWorker($this->timer);
$bounce = $this->workers_factory->createBounceWorker($this->timer);
return $bounce->process();
}
function executeMigrationWorker() {
$migration = new MigrationWorker($this->timer);
$migration = $this->workers_factory->createMigrationWorker($this->timer);
return $migration->process();
}

View File

@@ -8,6 +8,7 @@ use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Cron\Workers\Bounce as BounceWorker;
use MailPoet\Cron\Workers\KeyCheck\PremiumKeyCheck as PremiumKeyCheckWorker;
use MailPoet\Cron\Workers\KeyCheck\SendingServiceKeyCheck as SendingServiceKeyCheckWorker;
use MailPoet\Cron\Workers\StatsNotifications\Worker;
use MailPoet\Mailer\MailerLog;
use MailPoet\Models\Setting;
use MailPoet\Services\Bridge;
@@ -44,6 +45,8 @@ class WordPress {
$premium_key_specified = Bridge::isPremiumKeySpecified();
$premium_keycheck_due_tasks = PremiumKeyCheckWorker::getDueTasks();
$premium_keycheck_future_tasks = PremiumKeyCheckWorker::getFutureTasks();
// stats notifications
$stats_notifications_tasks = (bool)Worker::getDueTasks();
// check requirements for each worker
$sending_queue_active = (($scheduled_queues || $running_queues) && !$sending_limit_reached && !$sending_is_paused);
$bounce_sync_active = ($mp_sending_enabled && ($bounce_due_tasks || !$bounce_future_tasks));
@@ -57,6 +60,7 @@ class WordPress {
|| $bounce_sync_active
|| $sending_service_key_check_active
|| $premium_key_check_active
|| $stats_notifications_tasks
);
}

View File

@@ -5,6 +5,7 @@ use MailPoet\Cron\CronHelper;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Links;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Mailer as MailerTask;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Newsletter as NewsletterTask;
use MailPoet\Cron\Workers\StatsNotifications\Scheduler as StatsNotificationsScheduler;
use MailPoet\Logging\Logger;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\MailerLog;
@@ -26,11 +27,15 @@ class SendingQueue {
const BATCH_SIZE = 20;
const TASK_BATCH_SIZE = 5;
/** @var StatsNotificationsScheduler */
public $stats_notifications_scheduler;
/** @var SendingErrorHandler */
private $error_handler;
function __construct(SendingErrorHandler $error_handler, $timer = false, $mailer_task = false, $newsletter_task = false) {
function __construct(SendingErrorHandler $error_handler, StatsNotificationsScheduler $stats_notifications_scheduler, $timer = false, $mailer_task = false, $newsletter_task = false) {
$this->error_handler = $error_handler;
$this->stats_notifications_scheduler = $stats_notifications_scheduler;
$this->mailer_task = ($mailer_task) ? $mailer_task : new MailerTask();
$this->newsletter_task = ($newsletter_task) ? $newsletter_task : new NewsletterTask();
$this->timer = ($timer) ? $timer : microtime(true);
@@ -99,6 +104,7 @@ class SendingQueue {
);
if($queue->status === ScheduledTaskModel::STATUS_COMPLETED) {
$this->newsletter_task->markNewsletterAsSent($newsletter, $queue);
$this->stats_notifications_scheduler->schedule($newsletter);
}
$this->enforceSendingAndExecutionLimits();
}

View File

@@ -0,0 +1,84 @@
<?php
namespace MailPoet\Cron\Workers\StatsNotifications;
use Carbon\Carbon;
use MailPoet\Models\Newsletter;
use MailPoet\Models\ScheduledTask;
use MailPoet\Models\Setting;
use MailPoet\Models\StatsNotification;
class Scheduler {
/**
* How many hours after the newsletter will be the stats notification sent
* @var int
*/
const HOURS_TO_SEND_AFTER_NEWSLETTER = 24;
function schedule(Newsletter $newsletter) {
if(!$this->shouldSchedule($newsletter)) {
return false;
}
$task = ScheduledTask::create();
$task->type = Worker::TASK_TYPE;
$task->status = ScheduledTask::STATUS_SCHEDULED;
$task->scheduled_at = $this->getNextRunDate();
$task->save();
$stats_notifications = StatsNotification::create();
$stats_notifications->newsletter_id = $newsletter->id;
$stats_notifications->task_id = $task->id;
$stats_notifications->save();
}
private function shouldSchedule(Newsletter $newsletter) {
if($this->isDisabled()) {
return false;
}
if($this->isTaskScheduled($newsletter->id)) {
return false;
}
if(($newsletter->type !== Newsletter::TYPE_NOTIFICATION) && ($newsletter->type !== Newsletter::TYPE_STANDARD)) {
return false;
}
return true;
}
private function isDisabled() {
$settings = Setting::getValue(Worker::SETTINGS_KEY);
if(!is_array($settings)) {
return true;
}
if(!isset($settings['enabled'])) {
return true;
}
if(!isset($settings['address'])) {
return true;
}
if(empty(trim($settings['address']))) {
return true;
}
if(!(bool)Setting::getValue('tracking.enabled')) {
return true;
}
return !(bool)$settings['enabled'];
}
private function isTaskScheduled($newsletter_id) {
$existing = ScheduledTask::table_alias('tasks')
->join(StatsNotification::$_table, 'tasks.id = notification.task_id', 'notification')
->where('tasks.type', Worker::TASK_TYPE)
->where('notification.newsletter_id', $newsletter_id)
->findMany();
return (bool)$existing;
}
private function getNextRunDate() {
$date = new Carbon();
$date->addHours(self::HOURS_TO_SEND_AFTER_NEWSLETTER);
return $date;
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace MailPoet\Cron\Workers\StatsNotifications;
use Carbon\Carbon;
use MailPoet\Config\Renderer;
use MailPoet\Cron\CronHelper;
use MailPoet\Mailer\Mailer;
use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterLink;
use MailPoet\Models\ScheduledTask;
use MailPoet\Models\Setting;
use MailPoet\Tasks\Sending;
class Worker {
const TASK_TYPE = 'stats_notification';
const SETTINGS_KEY = 'stats_notifications';
const SENDER_EMAIL_PREFIX = 'wordpress@';
/** @var float */
public $timer;
/** @var Renderer */
private $renderer;
/** @var \MailPoet\Mailer\Mailer */
private $mailer;
function __construct(Mailer $mailer, Renderer $renderer, $timer = false) {
$this->timer = $timer ?: microtime(true);
$this->renderer = $renderer;
$this->mailer = $mailer;
}
/** @throws \Exception */
function process() {
$settings = Setting::getValue(self::SETTINGS_KEY);
$this->mailer->sender = $this->mailer->getSenderNameAndAddress($this->constructSenderEmail());
foreach(self::getDueTasks() as $task) {
try {
$this->mailer->send($this->constructNewsletter($task), $settings['address']);
} catch(\Exception $e) {
if(WP_DEBUG) {
throw $e;
}
} finally {
$this->markTaskAsFinished($task);
}
CronHelper::enforceExecutionLimit($this->timer);
}
}
private function constructSenderEmail() {
$url_parts = parse_url(home_url());
$site_name = strtolower($url_parts['host']);
if(strpos($site_name, 'www.') === 0) {
$site_name = substr($site_name, 4);
}
return [
'address' => self::SENDER_EMAIL_PREFIX . $site_name,
'name' => self::SENDER_EMAIL_PREFIX . $site_name,
];
}
public static function getDueTasks() {
$date = new Carbon();
return ScheduledTask::orderByAsc('priority')
->orderByAsc('updated_at')
->whereNull('deleted_at')
->where('status', ScheduledTask::STATUS_SCHEDULED)
->whereLte('scheduled_at', $date)
->where('type', self::TASK_TYPE)
->limit(Sending::RESULT_BATCH_SIZE)
->findMany();
}
private function constructNewsletter(ScheduledTask $task) {
$newsletter = $this->getNewsletter($task);
$link = NewsletterLink::findTopLinkForNewsletter($newsletter);
$context = $this->prepareContext($newsletter, $link);
return [
'subject' => sprintf(_x('Stats for email %s', 'title of an automatic email containing statistics (newsletter open rate, click rate, etc)', 'mailpoet'), $newsletter->subject),
'body' => [
'html' => $this->renderer->render('emails/statsNotification.html', $context),
'text' => $this->renderer->render('emails/statsNotification.txt', $context),
],
];
}
private function getNewsletter(ScheduledTask $task) {
$statsNotificationModel = $task->statsNotification()->findOne();
$newsletter = $statsNotificationModel->newsletter()->findOne();
if(!$newsletter) {
throw new \Exception('Newsletter not found');
}
return $newsletter
->withSendingQueue()
->withTotalSent()
->withStatistics();
}
private function prepareContext(Newsletter $newsletter, NewsletterLink $link = null) {
$clicked = ($newsletter->statistics['clicked'] * 100) / $newsletter->total_sent;
$opened = ($newsletter->statistics['opened'] * 100) / $newsletter->total_sent;
$unsubscribed = ($newsletter->statistics['unsubscribed'] * 100) / $newsletter->total_sent;
$context = [
'subject' => $newsletter->subject,
'preheader' => sprintf(_x(
'%1$s%% opens, %2$s%% clicks, %3$s%% unsubscribes in a nutshell.', 'newsletter open rate, click rate and unsubscribe rate', 'mailpoet'),
number_format($opened, 2),
number_format($clicked, 2),
number_format($unsubscribed, 2)
),
'topLinkClicks' => 0,
'linkSettings' => get_site_url(null, '/wp-admin/admin.php?page=mailpoet-settings#basics'),
'linkStats' => get_site_url(null, '/wp-admin/admin.php?page=mailpoet-newsletters#/stats/' . $newsletter->id()),
'premiumPage' => get_site_url(null, '/wp-admin/admin.php?page=mailpoet-premium'),
'premiumPluginActive' => is_plugin_active('mailpoet-premium/mailpoet-premium.php'),
'clicked' => $clicked,
'opened' => $opened,
];
if($link) {
$context['topLinkClicks'] = (int)$link->clicksCount;
$context['topLink'] = $link->url;
}
return $context;
}
private function markTaskAsFinished(ScheduledTask $task) {
$task->status = ScheduledTask::STATUS_COMPLETED;
$task->processed_at = new Carbon;
$task->scheduled_at = null;
$task->save();
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace MailPoet\Cron\Workers;
use MailPoet\Config\Renderer;
use MailPoet\Cron\Workers\StatsNotifications\Scheduler as StatsNotificationScheduler;
use MailPoet\Cron\Workers\Scheduler as SchedulerWorker;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Cron\Workers\SendingQueue\Migration as MigrationWorker;
use MailPoet\Cron\Workers\StatsNotifications\Worker as StatsNotificationsWorker;
use MailPoet\Cron\Workers\Bounce as BounceWorker;
use MailPoet\Cron\Workers\KeyCheck\PremiumKeyCheck as PremiumKeyCheckWorker;
use MailPoet\Cron\Workers\KeyCheck\SendingServiceKeyCheck as SendingServiceKeyCheckWorker;
use MailPoet\Cron\Workers\SendingQueue\SendingErrorHandler;
use MailPoet\Mailer\Mailer;
class WorkersFactory {
/** @var SendingErrorHandler */
private $sending_error_handler;
/** @var StatsNotificationScheduler */
private $scheduler;
/** @var Mailer */
private $mailer;
/**
* @var Renderer
*/
private $renderer;
public function __construct(SendingErrorHandler $sending_error_handler, StatsNotificationScheduler $scheduler, Mailer $mailer, Renderer $renderer) {
$this->sending_error_handler = $sending_error_handler;
$this->scheduler = $scheduler;
$this->mailer = $mailer;
$this->renderer = $renderer;
}
/** @return SchedulerWorker */
function createScheduleWorker($timer) {
return new SchedulerWorker($timer);
}
/** @return SendingQueueWorker */
function createQueueWorker($timer) {
return new SendingQueueWorker($this->sending_error_handler, $this->scheduler, $timer);
}
function createStatsNotificationsWorker($timer) {
return new StatsNotificationsWorker($this->mailer, $this->renderer, $timer);
}
/** @return SendingServiceKeyCheckWorker */
function createSendingServiceKeyCheckWorker($timer) {
return new SendingServiceKeyCheckWorker($timer);
}
/** @return PremiumKeyCheckWorker */
function createPremiumKeyCheckWorker($timer) {
return new PremiumKeyCheckWorker($timer);
}
/** @return BounceWorker */
function createBounceWorker($timer) {
return new BounceWorker($timer);
}
/** @return MigrationWorker */
function createMigrationWorker($timer) {
return new MigrationWorker($timer);
}
}