373 lines
12 KiB
PHP
373 lines
12 KiB
PHP
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
|
|
|
namespace MailPoet\Tasks;
|
|
|
|
if (!defined('ABSPATH')) exit;
|
|
|
|
|
|
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueAlias;
|
|
use MailPoet\DI\ContainerWrapper;
|
|
use MailPoet\Entities\ScheduledTaskEntity;
|
|
use MailPoet\Entities\ScheduledTaskSubscriberEntity;
|
|
use MailPoet\Entities\SendingQueueEntity;
|
|
use MailPoet\InvalidStateException;
|
|
use MailPoet\Logging\LoggerFactory;
|
|
use MailPoet\Models\ScheduledTask;
|
|
use MailPoet\Models\SendingQueue;
|
|
use MailPoet\Newsletter\Sending\ScheduledTasksRepository;
|
|
use MailPoet\Newsletter\Sending\ScheduledTaskSubscribersRepository;
|
|
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
|
|
use MailPoet\Util\Helpers;
|
|
|
|
/**
|
|
* A facade class containing all necessary models to work with a sending queue
|
|
* @property string|null $status
|
|
* @property int $taskId
|
|
* @property int $id
|
|
* @property int $newsletterId
|
|
* @property string $newsletterRenderedSubject
|
|
* @property string|array $newsletterRenderedBody
|
|
* @property bool $nonExistentColumn
|
|
* @property string $scheduledAt
|
|
* @property int $priority
|
|
*/
|
|
class Sending {
|
|
const TASK_TYPE = SendingQueueAlias::TASK_TYPE;
|
|
|
|
/** @var ScheduledTask */
|
|
private $task;
|
|
|
|
/** @var SendingQueue */
|
|
private $queue;
|
|
|
|
private $queueFields = [
|
|
'id',
|
|
'task_id',
|
|
'newsletter_id',
|
|
'newsletter_rendered_subject',
|
|
'newsletter_rendered_body',
|
|
'count_total',
|
|
'count_processed',
|
|
'count_to_process',
|
|
'meta',
|
|
];
|
|
|
|
private $commonFields = [
|
|
'created_at',
|
|
'updated_at',
|
|
'deleted_at',
|
|
];
|
|
|
|
/** @var ScheduledTaskSubscribersRepository */
|
|
private $scheduledTaskSubscribersRepository;
|
|
|
|
/** @var ScheduledTasksRepository */
|
|
private $scheduledTasksRepository;
|
|
|
|
/** @var ScheduledTaskEntity */
|
|
private $scheduledTaskEntity;
|
|
|
|
/** @var SendingQueuesRepository */
|
|
private $sendingQueuesRepository;
|
|
|
|
private function __construct(
|
|
ScheduledTask $task = null,
|
|
SendingQueue $queue = null
|
|
) {
|
|
if (!$task instanceof ScheduledTask) {
|
|
$task = ScheduledTask::create();
|
|
$task->type = self::TASK_TYPE;
|
|
$task->save();
|
|
}
|
|
if (!$queue instanceof SendingQueue) {
|
|
$queue = SendingQueue::create();
|
|
$queue->newsletterId = 0;
|
|
$queue->taskId = $task->id;
|
|
$queue->save();
|
|
}
|
|
|
|
if ($task->type !== self::TASK_TYPE) {
|
|
throw new \Exception('Only tasks of type "' . self::TASK_TYPE . '" are accepted by this class');
|
|
}
|
|
|
|
$this->task = $task;
|
|
$this->queue = $queue;
|
|
$this->scheduledTaskSubscribersRepository = ContainerWrapper::getInstance()->get(ScheduledTaskSubscribersRepository::class);
|
|
$this->scheduledTasksRepository = ContainerWrapper::getInstance()->get(ScheduledTasksRepository::class);
|
|
$this->sendingQueuesRepository = ContainerWrapper::getInstance()->get(SendingQueuesRepository::class);
|
|
|
|
// needed to make sure that the task has an ID so that we can retrieve the ScheduledTaskEntity while this class still uses Paris
|
|
$this->save();
|
|
|
|
$scheduledTaskEntity = $this->scheduledTasksRepository->findOneById($this->task->id);
|
|
|
|
if (!$scheduledTaskEntity instanceof ScheduledTaskEntity) {
|
|
throw new InvalidStateException('Scheduled task entity not found');
|
|
}
|
|
|
|
$this->scheduledTaskEntity = $scheduledTaskEntity;
|
|
}
|
|
|
|
public static function create(ScheduledTask $task = null, SendingQueue $queue = null) {
|
|
return new self($task, $queue);
|
|
}
|
|
|
|
public static function createManyFromTasks($tasks) {
|
|
if (empty($tasks)) {
|
|
return [];
|
|
}
|
|
|
|
$tasksIds = array_map(function($task) {
|
|
return $task->id;
|
|
}, $tasks);
|
|
|
|
$queues = SendingQueue::whereIn('task_id', $tasksIds)->findMany();
|
|
$queuesIndex = [];
|
|
foreach ($queues as $queue) {
|
|
$queuesIndex[$queue->taskId] = $queue;
|
|
}
|
|
|
|
$result = [];
|
|
foreach ($tasks as $task) {
|
|
if (!empty($queuesIndex[$task->id])) {
|
|
$result[] = self::create($task, $queuesIndex[$task->id]);
|
|
} else {
|
|
static::handleInvalidTask($task);
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
public static function handleInvalidTask(ScheduledTask $task) {
|
|
$loggerFactory = LoggerFactory::getInstance();
|
|
$loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->error(
|
|
'invalid sending task found',
|
|
['task_id' => $task->id]
|
|
);
|
|
$task->status = ScheduledTask::STATUS_INVALID;
|
|
$task->save();
|
|
}
|
|
|
|
public static function createFromScheduledTask(ScheduledTask $task) {
|
|
$queue = SendingQueue::where('task_id', $task->id)->findOne();
|
|
if (!$queue) {
|
|
return false;
|
|
}
|
|
|
|
return self::create($task, $queue);
|
|
}
|
|
|
|
public static function createFromQueue(SendingQueue $queue) {
|
|
$task = $queue->task()->findOne();
|
|
if (!$task) {
|
|
return false;
|
|
}
|
|
|
|
return self::create($task, $queue);
|
|
}
|
|
|
|
public static function getByNewsletterId($newsletterId) {
|
|
$queue = SendingQueue::where('newsletter_id', $newsletterId)
|
|
->orderByDesc('updated_at')
|
|
->findOne();
|
|
if (!$queue instanceof SendingQueue) {
|
|
return false;
|
|
}
|
|
|
|
return self::createFromQueue($queue);
|
|
}
|
|
|
|
public function asArray() {
|
|
$queue = array_intersect_key(
|
|
$this->queue->asArray(),
|
|
array_flip($this->queueFields)
|
|
);
|
|
$task = $this->task->asArray();
|
|
return array_merge($task, $queue);
|
|
}
|
|
|
|
public function getErrors() {
|
|
$queueErrors = $this->queue->getErrors();
|
|
$taskErrors = $this->task->getErrors();
|
|
if (empty($queueErrors) && empty($taskErrors)) {
|
|
return false;
|
|
}
|
|
return array_merge((array)$queueErrors, (array)$taskErrors);
|
|
}
|
|
|
|
public function save() {
|
|
$this->queue->save();
|
|
$this->task->save();
|
|
$errors = $this->getErrors();
|
|
if ($errors) {
|
|
$loggerFactory = LoggerFactory::getInstance();
|
|
$loggerFactory->getLogger(LoggerFactory::TOPIC_NEWSLETTERS)->error(
|
|
'error saving sending task',
|
|
['task_id' => $this->task->id, 'queue_id' => $this->queue->id, 'errors' => $errors]
|
|
);
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
public function delete() {
|
|
$this->scheduledTaskSubscribersRepository->deleteByScheduledTask($this->scheduledTaskEntity);
|
|
$this->scheduledTasksRepository->remove($this->scheduledTaskEntity);
|
|
|
|
$sendingQueueEntity = $this->scheduledTaskEntity->getSendingQueue();
|
|
|
|
if ($sendingQueueEntity) {
|
|
$this->sendingQueuesRepository->remove($sendingQueueEntity);
|
|
}
|
|
|
|
$this->scheduledTasksRepository->flush();
|
|
}
|
|
|
|
public function queue() {
|
|
return $this->queue;
|
|
}
|
|
|
|
public function getSendingQueueEntity(): SendingQueueEntity {
|
|
$sendingQueueEntity = $this->sendingQueuesRepository->findOneById($this->queue->id);
|
|
if (!$sendingQueueEntity) {
|
|
throw new InvalidStateException();
|
|
}
|
|
$this->sendingQueuesRepository->refresh($sendingQueueEntity);
|
|
|
|
return $sendingQueueEntity;
|
|
}
|
|
|
|
public function task() {
|
|
return $this->task;
|
|
}
|
|
|
|
public function getSubscribers($processed = null) {
|
|
if (is_null($processed)) {
|
|
$subscribers = $this->scheduledTaskSubscribersRepository->findBy(['task' => $this->task->id]);
|
|
} else if ($processed) {
|
|
$subscribers = $this->scheduledTaskSubscribersRepository->findBy(
|
|
['task' => $this->task->id, 'processed' => ScheduledTaskSubscriberEntity::STATUS_PROCESSED]
|
|
);
|
|
} else {
|
|
$subscribers = $this->scheduledTaskSubscribersRepository->findBy(
|
|
['task' => $this->task->id, 'processed' => ScheduledTaskSubscriberEntity::STATUS_UNPROCESSED]
|
|
);
|
|
}
|
|
|
|
return array_map(
|
|
function(ScheduledTaskSubscriberEntity $scheduledTaskSubscriber) {
|
|
return (string)$scheduledTaskSubscriber->getSubscriberId();
|
|
},
|
|
$subscribers
|
|
);
|
|
}
|
|
|
|
public function setSubscribers(array $subscriberIds) {
|
|
$this->scheduledTaskSubscribersRepository->setSubscribers($this->scheduledTaskEntity, $subscriberIds);
|
|
$this->updateCount();
|
|
}
|
|
|
|
public function removeSubscribers(array $subscriberIds) {
|
|
$this->scheduledTaskSubscribersRepository->deleteByScheduledTaskAndSubscriberIds($this->scheduledTaskEntity, $subscriberIds);
|
|
|
|
$this->updateTaskStatus();
|
|
$this->updateCount();
|
|
}
|
|
|
|
public function updateProcessedSubscribers(array $processedSubscribers): bool {
|
|
$this->scheduledTaskSubscribersRepository->updateProcessedSubscribers($this->scheduledTaskEntity, $processedSubscribers);
|
|
$this->scheduledTasksRepository->refresh($this->scheduledTaskEntity); // needed while Sending still uses Paris
|
|
$this->status = $this->scheduledTaskEntity->getStatus();
|
|
return $this->updateCount(count($processedSubscribers))->getErrors() === false;
|
|
}
|
|
|
|
public function saveSubscriberError($subcriberId, $errorMessage) {
|
|
$this->scheduledTaskSubscribersRepository->saveError($this->scheduledTaskEntity, $subcriberId, $errorMessage);
|
|
|
|
$this->updateTaskStatus();
|
|
|
|
return $this->updateCount()->getErrors() === false;
|
|
}
|
|
|
|
private function updateTaskStatus() {
|
|
// we need to update those fields here as the Sending class is in a mixed state using Paris and Doctrine at the same time
|
|
// this probably won't be necessary anymore once https://mailpoet.atlassian.net/browse/MAILPOET-4375 is finished
|
|
$this->task->status = $this->scheduledTaskEntity->getStatus();
|
|
if (!is_null($this->scheduledTaskEntity->getProcessedAt())) {
|
|
$this->task->processedAt = $this->scheduledTaskEntity->getProcessedAt()->format('Y-m-d H:i:s');
|
|
}
|
|
}
|
|
|
|
public function updateCount(?int $count = null) {
|
|
if ($count) {
|
|
// increment/decrement counts based on known subscriber count, don't exceed the bounds
|
|
$this->queue->countProcessed = min($this->queue->countProcessed + $count, $this->queue->countTotal);
|
|
$this->queue->countToProcess = max($this->queue->countToProcess - $count, 0);
|
|
} else {
|
|
// query DB to update counts, slower but more accurate, to be used if count isn't known
|
|
$this->queue->countProcessed = $this->scheduledTaskSubscribersRepository->countProcessed($this->scheduledTaskEntity);
|
|
$this->queue->countToProcess = $this->scheduledTaskSubscribersRepository->countUnprocessed($this->scheduledTaskEntity);
|
|
$this->queue->countTotal = $this->queue->countProcessed + $this->queue->countToProcess;
|
|
}
|
|
return $this->queue->save();
|
|
}
|
|
|
|
public function hydrate(array $data) {
|
|
foreach ($data as $k => $v) {
|
|
$this->__set($k, $v);
|
|
}
|
|
}
|
|
|
|
public function validate() {
|
|
return $this->queue->validate() && $this->task->validate();
|
|
}
|
|
|
|
public function getMeta() {
|
|
return $this->queue->getMeta();
|
|
}
|
|
|
|
public function __isset($prop) {
|
|
$prop = Helpers::camelCaseToUnderscore($prop);
|
|
if ($this->isQueueProperty($prop)) {
|
|
return isset($this->queue->$prop);
|
|
} else {
|
|
return isset($this->task->$prop);
|
|
}
|
|
}
|
|
|
|
public function __get($prop) {
|
|
$prop = Helpers::camelCaseToUnderscore($prop);
|
|
if ($this->isQueueProperty($prop)) {
|
|
return $this->queue->$prop;
|
|
} else {
|
|
return $this->task->$prop;
|
|
}
|
|
}
|
|
|
|
public function __set($prop, $value) {
|
|
$prop = Helpers::camelCaseToUnderscore($prop);
|
|
if ($this->isCommonProperty($prop)) {
|
|
$this->queue->$prop = $value;
|
|
$this->task->$prop = $value;
|
|
} elseif ($this->isQueueProperty($prop)) {
|
|
$this->queue->$prop = $value;
|
|
} else {
|
|
$this->task->$prop = $value;
|
|
}
|
|
}
|
|
|
|
public function __call($name, $args) {
|
|
$obj = method_exists($this->queue, $name) ? $this->queue : $this->task;
|
|
$callback = [$obj, $name];
|
|
if (is_callable($callback)) {
|
|
return call_user_func_array($callback, $args);
|
|
}
|
|
}
|
|
|
|
private function isQueueProperty($prop) {
|
|
return in_array($prop, $this->queueFields);
|
|
}
|
|
|
|
private function isCommonProperty($prop) {
|
|
return in_array($prop, $this->commonFields);
|
|
}
|
|
}
|