В предыдущей статье мы создали форму подписки для сайта и написали функционал, который проверяет введенные посетителем данные, сохраняет в БД и выводит уведомления без перезагрузки страницы. Теперь в базе данных у нас есть e-mail подписчика и дата подписки (в виде метки времени UNIX). Осталось организовать саму рассылку. Дело это не такое уж и простое.

Есть два варианта создания рассылки писем подписчикам сайта:

  • использование стороннего сервиса;
  • написание своего функционала рассылки.
По-поводу первого варианта могу сказать что у него есть несколько минусов. Вот недавно как раз читал, что закрылся один из таких популярнейших сервисов. При этом администрация предупреждала, что с такого-то числа все данные по подписчикам будут удалены и нужно было срочно импортировать свои данные. То есть, при использовании такого способа, ваша подписка будет зависима от внешних факторов. Так же в отсылаемые письма будут вставляться чужие ссылки. Ну а платные сервисы понятно какой минус имею.

В данной статье мы напишем скрипт своей рассылки, запускать который можно либо вручную, либо повесив на какое-либо событие (к примеру когда посетитель открывает какую-то, либо любую, страницу), или (оптимальный вариант) прописав время выполнения скрипта по расписанию в Cron.

Начнем.

Прежде всего создадим таблицу send_subscr. Для этого выполним миграцию (или создав нужные поля вручную):

public function safeUp()
{
    $tableOptions = null;
    if ($this->db->driverName === 'mysql') {
        $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
    }
    $this->createTable('{{%send_subscr}}', [
        'id' => $this->primaryKey(),
        'post_id' => $this->integer()->notNull(),
        'subscriber_id' => $this->integer()->notNull(),
        'end' => $this->boolean()->defaultValue(0),
    ], $tableOptions);
}
public function safeDown()
{
    $this->dropTable('{{%send_subscr}}');
}
Таблица имеет следующие поля:

  • id - уникальный идентификатор последней рассылки. Каждая строка рассылки будет соответствовать одному посту по которому будут рассылаться уведомления, то есть строки будут добавляться только когда найден новый пост.
  • post_id - уникальный идентификатор статьи (поста). Id поста будем брать из соответствующей таблицы. У меня это "post".
  • subscriber_id - уникальный идентификатор последнего подписчика, которому был отправлен выпуск. Данные будем брать из таблицы, которую создавали в предыдущей статье, когда создавали форму подписки "subscription".
  • end - закончена рассылка или нет (1-закончена, 0- нет).
Теперь нужно создать контроллер и модель, которые будут обрабатывать нашу рассылку. Создавать их будем в консольной среде, т.к. консольные приложения обычно и используются для создания фоновых и служебных задач, поддерживающих сайт.




Создание модели.

Код модели будет максимально прокомментирован, т.к. он не совсем прост для понимания.В папке console\models создадим файл модели SendSubscr.php с содержимым:



<?php
namespace console\models;
use Yii;
use common\models\Post;
use common\models\Subscription;

class SendSubscr extends \yii\db\ActiveRecord
{
    public static function tableName()
    {
        return '{{%send_subscr}}';
    }
    public function rules()
    {
        return [
            [['post_id', 'subscriber_id'], 'required'],
            [['post_id', 'subscriber_id'], 'integer'],
            [['end'],'boolean'],
        ];
    }
    public function attributeLabels()
    {
        return [
            'id' => 'ID',
            'post_id' => 'Post ID',
            'subscriber_id' => 'Subscriber ID',
            'end' => 'End',
        ];
    }

    public function send(){
        //В таблице send_subscr выбираем строку с самым большим id поста (последний пост по которому была рассылка)
        $query = $this->find()
                    ->limit(1)
                    ->orderBy('post_id DESC')
                    ->all();
        $last_subscribe = $query[0];

        //Если в этой строке по данному посту нет неотправленных уведомлений или если массива еще нет (1-й раз только)
        if ($last_subscribe->end == 1 or count($last_subscribe) == 0) {
            //Получаем id последнего поста по которому была рассылка или ставим 0, если еще не было.
            $last_post = $last_subscribe->post_id or $last_post = 0;
            //Проверяем есть ли в таблице post посты с id большим чем id полученного нами поста из таблицы
            //SendSubscr (то есть новые посты) со статусом PUBLISH
            $post = Post::find()
                    ->limit(1)
                    ->where(['>', 'id', $last_post])
                    ->andWhere(['publish_status' => 'PUBLISH'])
                    ->all();
            //Если нет новых постов, то выходим
            if (!$post) exit;
            //Если новые посты есть, то ставим в очередь В ТАБЛИЦУ send_subscr (создаем новую строку) и выходим
            $this->post_id = $post[0]->id;
            $this->subscriber_id = 0;
            $this->end = 0;
            $this->save();
            //Выходим, а через 10 минут данный скрипт будет снова запущен, и уже пойдёт непосредственно рассылка писем
            exit;
        }

        //Сюда выполнение доходит если есть незаконченные рассылки
        //Получаем id подписчика, которому было отправлено письмо в последний раз
        $last_id = $last_subscribe->subscriber_id;
        //В таблице ПОДПИСЧИКОВ получаем все строки, у которых id > id последнего подписчика из таблицы SendSubscr,
        //то есть которым не отправлено еще уведомление
        $subscriptions = Subscription::find()
                        ->where(['>', 'id', $last_id])
                        ->all();
        $max_count = count($subscriptions);
        //Будем отправлять за 1 раз 10 писем, но если подписчиков < 10, то ставим столько, сколько их.
        if ($max_count > 10) $max_count = 10;
        //перебираем подписчиков, отправляем им письма
        foreach ($subscriptions as $key => $sub){
            //получаем id строки в таблице subscription, он же  номер подписчика
            $subscriber_id = $sub->id;

         /* Метод отправки сообщения.
            Можно использовать данные подписчика: 
            $sub->email;
            $sub->addtime;
            ID поста для ссылки на страницу новой статьи - $last_subscribe->post_id */ 
            $this->sendSub($sub->email, $last_subscribe->post_id);
            //Если достигло максимума (10) отправок, то устанавливаем в ячейку subscriber_id номер текущего
            //подписчика в строке с текущей рассылкой и выходим из цикла    
            if ($key >= ($max_count-1)) {
                $send_subscr = self::findOne($last_subscribe->id);
                $send_subscr->subscriber_id = $subscriber_id;
                $send_subscr->update();
                break;
            }
        }
        //Если осталось менее 10 подписчиков которым еще не отправлено уведомление,
        //закрываем строку с текущей рассылкой, В ТАБЛИЦЕ send_subscr ставим `end`='1'  в строке с текущей рассылкой 
        if(count($subscriptions) <= $max_count)    {
            $send_subscr = self::findOne($last_subscribe->id);
            $send_subscr->end = 1;
            $send_subscr->update();
        }    
    }

    public function sendSub($email,$post_id){
        $home_url = 'http://yoursite.com';
        //Формирование ссылки на страницу поста
        $link = 'post/view?id=';
        $full_link = $link.$post_id;
        $url = \common\models\Sef::findOne(['link' => $full_link])->link_sef;
        $post_url = $home_url.'/'.$url.'.html';
        $msg = "Здравствуйте! Вы подписаны на рассылку уведомлений о новых статьях по WEB-программированию на сайте $home_url. Сообщаем, что опубликована новая статья. Для просмотра перейдите на $post_url";
        $msg_html  = "<html><body style='font-family:Arial,sans-serif;'>";
        $msg_html .= "<h3 style='font-weight:bold;border-bottom:1px dotted #ccc;'>Здравствуйте! Вы подписаны на рассылку уведомлений о новых статьях по WEB-программированию на сайте " . $home_url . "</h3>\r\n";
        $msg_html .= "<p><strong>Сообщаем, что опубликована новая статья. Для просмотра перейдите на </strong><a href='". $post_url ."'>$post_url</a></p>\r\n";
        $msg_html .= "</body></html>";
        Yii::$app->mailer->compose()
            //->setFrom('admin@yoursite.com') //e-mail админа, не указываем, если указано в common\config\main-local.php
            ->setTo($email) // кому отправляем - реальный адрес куда придёт письмо формата asdf@asdf.com
            ->setSubject('Уведомление о новой WEB-статье') // тема письма
            ->setTextBody($msg) // текст письма без HTML
            ->setHtmlBody($msg_html) // текст письма с HTML
            ->send();    
    }
}
Тут нас больше всего интересует метод send(), который при каждом запуске скрипта проверяет наличие новые постов на нашем сайте и добавляет их в очередь на рассылку. Далее вызывается метод sendSub(), который и отправляет письма используя для этого стандартный класс Yii2 Mailer. При этом за 1 раз будет отправлено только 10 писем, чтобы обойти ограничение хостингов.

Таким образом, скрипт можно запускать (автоматически), например раз в 10 минут или раз в час (в зависимости от кол-ва подписчиков) и он будет отправлять письма с перерывами, а когда отправит все, проверит, нет ли в БД новых постов. Если есть - добавляет в таблицу новую строку и начинает рассылку опять.

В методе sendSub() мы из id поста формируем полную ссылку на страницу и сохраняем в переменную $post_url. У меня каждая статья имеет буквенное название (алиас) с префиксом html на конце, поэтому я получаю его из таблицы Sef. Вы можете подстроить под себя, главное, чтобы в переменную $post_url попадала ссылка на данный пост, чтобы посетитель сайта, получив письмо смог перейти по ней. Как создать ЧПУ ссылку с буквенными названиями страниц читайте тут.




Создание контроллера.

В папке console\controllers создайте файл SendController.php с таким содержимым:

<?php
namespace console\controllers;
 
use Yii;
use yii\console\Controller;
use console\models\SendSubscr;
 
 
class SendController extends Controller
{
    public $defaultAction = 'mail';
 
    public function actionMail(){    
        $model = new SendSubscr();
        $model->send(); 
        return 0;
    }
}
Тут все просто - одно действие actionMail(), в котором создаем объект класса модели SendSubscr() и вызывается метод send(). Для удобства назначаем наш единственный метод действием по-умолчанию указанием переменной $defaultAction. В консольном приложении принято использовать код возврата. Ноль означает, что команда выполнена удачно.

Функционал рассылки готов, для запуска нужно ввести в консоли:

yii send
Ну, а чтобы не писать это каждый раз вручную, лучше всего создать расписание выполнения скрипта на хостинге с помощью Cron.