bizley/yii2-podium

View on GitHub
src/console/QueueController.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

namespace bizley\podium\console;

use bizley\podium\log\Log;
use bizley\podium\models\Email;
use Exception;
use Yii;
use yii\base\Action;
use yii\console\Controller;
use yii\db\Connection;
use yii\db\Query;
use yii\di\Instance;
use yii\helpers\Console;
use yii\mail\BaseMailer;
use bizley\podium\Podium;

/**
 * Podium command line tool to send emails.
 *
 * @author Paweł Bizley Brzozowski <pawel@positive.codes>
 * @since 0.1
 */
class QueueController extends Controller
{

    const DEFAULT_BATCH_LIMIT = 100;

    /**
     * @var Connection|array|string|null the DB connection object (or its
     * configuration array) or the application component ID of the DB connection
     * to use when reading emails queue.
     * By default module DB connection is used.
     */
    public $db;

    /**
     * @var string controller default action ID.
     */
    public $defaultAction = 'run';

    /**
     * @var int the limit of emails sent in one batch (default 100).
     */
    public $limit = self::DEFAULT_BATCH_LIMIT;

    /**
     * @var BaseMailer|array|string the BaseMailer object (or its configuration
     * array) or the application component ID of the mailer to use when sending
     * emails.
     */
    public $mailer = 'mailer';

    /**
     * @var string the name of the table for email queue.
     */
    public $queueTable = '{{%podium_email}}';

    /**
     * @inheritdoc
     */
    public function options($actionID)
    {
        return array_merge(parent::options($actionID), ['queueTable', 'db', 'mailer']);
    }

    /**
     * Checks the existence of the db and mailer components.
     * @param Action $action the action to be executed.
     * @return bool whether the action should continue to be executed.
     */
    public function beforeAction($action)
    {
        try {
            if (parent::beforeAction($action)) {
                $this->db = !$this->db ? Podium::getInstance()->getDb() : Instance::ensure($this->db, Connection::className());
                $this->mailer = Instance::ensure($this->mailer, BaseMailer::className());
                return true;
            }
        } catch (Exception $e) {
            $this->stderr("ERROR: " . $e->getMessage() . "\n");
        }
        return false;
    }

    /**
     * Returns new batch of emails.
     * @param int $limit maximum number of rows in batch
     * @return array
     */
    public function getNewBatch($limit = 0)
    {
        try {
            if (!is_numeric($limit) || $limit <= 0) {
                $limit = $this->limit;
            }
            return (new Query)
                    ->from($this->queueTable)
                    ->where(['status' => Email::STATUS_PENDING])
                    ->orderBy(['id' => SORT_ASC])
                    ->limit((int)$limit)
                    ->all($this->db);
        } catch (Exception $e) {
            Log::error($e->getMessage(), null, __METHOD__);
        }
    }

    /**
     * Sends email using mailer component.
     * @param string $email
     * @param string $fromName
     * @param string $fromEmail
     * @return bool
     */
    public function send($email, $fromName, $fromEmail)
    {
        try {
            $mailer = Yii::$app->mailer->compose();
            $mailer->setFrom([$fromEmail => $fromName]);
            $mailer->setTo($email['email']);
            $mailer->setSubject($email['subject']);
            $mailer->setHtmlBody($email['content']);
            $mailer->setTextBody(strip_tags(str_replace(
                ['<br>', '<br/>', '<br />', '</p>'],
                "\n",
                $email['content']
            )));
            return $mailer->send();
        } catch (Exception $e) {
            Log::error($e->getMessage(), null, __METHOD__);
        }
    }

    /**
     * Tries to send email from queue and updates its status.
     * @param string $email
     * @param string $fromName
     * @param string $fromEmail
     * @param int $maxAttempts
     * @return bool
     */
    public function process($email, $fromName, $fromEmail, $maxAttempts)
    {
        try {
            if ($this->send($email, $fromName, $fromEmail)) {
                $this
                    ->db
                    ->createCommand()
                    ->update(
                        $this->queueTable,
                        ['status' => Email::STATUS_SENT],
                        ['id' => $email['id']]
                    )
                    ->execute();
                return true;
            }

            $attempt = $email['attempt'] + 1;
            if ($attempt <= $maxAttempts) {
                $this
                    ->db
                    ->createCommand()
                    ->update(
                        $this->queueTable,
                        ['attempt' => $attempt],
                        ['id' => $email['id']]
                    )
                    ->execute();
            } else {
                $this
                    ->db
                    ->createCommand()
                    ->update(
                        $this->queueTable,
                        ['status' => Email::STATUS_GAVEUP],
                        ['id' => $email['id']]
                    )
                    ->execute();
            }
        } catch (Exception $e) {
            Log::error($e->getMessage(), null, __METHOD__);
        }
        return false;
    }

    /**
     * Runs the queue.
     * @param int $limit
     * @return int|void
     */
    public function actionRun($limit = 0)
    {
        $version = $this->module->version;
        $this->stdout("\nPodium mail queue v{$version}\n");
        $this->stdout("------------------------------\n");

        $emails = $this->getNewBatch($limit);
        if (empty($emails)) {
            $this->stdout("No pending emails in the queue found.\n\n", Console::FG_GREEN);
            return self::EXIT_CODE_NORMAL;
        }

        $total = count($emails);
        $this->stdout(
            "\n$total pending "
                . ($total === 1 ? 'email' : 'emails')
                . " to be sent now:\n",
            Console::FG_YELLOW
        );

        $errors = false;
        foreach ($emails as $email) {
            if (!$this->process(
                    $email,
                    $this->module->podiumConfig->get('from_name'),
                    $this->module->podiumConfig->get('from_email'),
                    $this->module->podiumConfig->get('max_attempts')
                )) {
                $errors = true;
            }
        }

        if ($errors) {
            $this->stdout("\nBatch sent with errors.\n\n", Console::FG_RED);
        } else {
            $this->stdout("\nBatch sent successfully.\n\n", Console::FG_GREEN);
        }
        return self::EXIT_CODE_NORMAL;
    }

    /**
     * Checks the current status for the mail queue.
     */
    public function actionCheck()
    {
        $version = $this->module->version;
        $this->stdout("\nPodium mail queue check v{$version}\n");
        $this->stdout("------------------------------\n");
        $this->stdout(" EMAILS  | COUNT\n");
        $this->stdout("------------------------------\n");

        $pending = (new Query)
                    ->from($this->queueTable)
                    ->where(['status' => Email::STATUS_PENDING])
                    ->count('id', $this->db);
        $sent = (new Query)
                    ->from($this->queueTable)
                    ->where(['status' => Email::STATUS_SENT])
                    ->count('id', $this->db);
        $gaveup = (new Query)
                    ->from($this->queueTable)
                    ->where(['status' => Email::STATUS_GAVEUP])
                    ->count('id', $this->db);

        $showPending = $this->ansiFormat($pending, Console::FG_YELLOW);
        $showSent = $this->ansiFormat($sent, Console::FG_GREEN);
        $showGaveup = $this->ansiFormat($gaveup, Console::FG_RED);

        $this->stdout(" pending | $showPending\n");
        $this->stdout(" sent    | $showSent\n");
        $this->stdout(" stucked | $showGaveup\n");
        $this->stdout("------------------------------\n\n");
        return self::EXIT_CODE_NORMAL;
    }
}