Самостоятельная подписка и отписка пользователя

Мы плавно подходим к окончанию разработки компонента и проведения уроков. Сегодня не будет ничего нового просто делаем самостоятельную подписку и отписку пользователя.

Для этого нужно будет добавить новое поле code в объект sxSubscriber (для ссылки "отписаться от рассылки"), прописать в классе sxNewsletter новые методы для проверки почты и подписки\отписки и добавить обработку этих действий в сниппет Sendex.

В общем, ничего интересного, обычное программирование на PHP.

Добавляем поле code

Редактируем нашу схему в файле /core/components/sendex/model/schema/sendex.mysql.schema.xml, добавляем поле и индекс:


<object class="sxSubscriber" table="sendex_subscribers" extends="xPDOSimpleObject">
    ....
    <field key="code" dbtype="char" precision="40" phptype="string" null="true" default="" />

    ....
    <index alias="code" name="code" primary="false" unique="true" type="BTREE">
        <column key="code" length="" collation="A" null="false" />
    </index>
</object>

Добавляем создание поля и индекса в ресолвере /_build/resolvers/resolve.tables.php.


    $manager->addField('sxSubscriber', 'code');
    $manager->addIndex('sxSubscriber', 'code');

И запускаем сборку пакета (/Sendex/_build/build.transport.php), которая перегенерирует модель и обновит таблицу БД.

Как видите, с каждым разом это всё проще и проще. Вот коммит.

Методы в sxNewsletter

Редактируем файл /core/components/sendex/model/sendex/sxnewsletter.class.php.

Всего мы добавляем 4 метода.

checkEmail

Метод для проверки email пользователя.

Здесь используется очень интересный сервис MODX modRegistry. Это специальная система MODX, куда вносятся временные данные, которые потом можно проверить и удалить. Например, через modRegistry работает сброс паролей.


// Подключем сервис
$registry = $this->xpdo->getService('registry', 'registry.modRegistry');
$instance = $registry->getRegister('user', 'registry.modDbRegister');
$instance->connect();

// Создаём свой канал
$instance->subscribe('/sendex/subscribe/');
// Сохраняем нужные данные
$instance->send('/sendex/subscribe/',
    array(
        $hash => array(
            'user_id' => $user_id,
            'newsletter_id' => $this->id,
            'email' => $email,
        )
    ),
    array(
        'ttl' => $linkTTL
    )
);

Теперь в течении времени ttl можно получить сохранённые данные в следующем методе.

confirmEmail

Метод для подтверждения email пользователя. К нему приходит ссылка, по которой он должен перейти с уникальным hash - его мы сохранили в modRegistry в предыдущем методе.


// Подключем сервис
// ...
// Подписываемся на канал, указывая уникальный хэш из письма
$instance->subscribe('/sendex/subscribe/' . $hash);

// Читаем данные и удаляем после этого.
$entry = $instance->read(array('poll_limit' => 1));
// Если код верный, и мы что-то прочитали - проверяем и вызываем следующий метод
if (!empty($entry[0])) {
    $entry = reset($entry);
    if ($this->id != $entry['newsletter_id']) {
        /** @var sxNewsletter $newsletter */
        if ($newsletter = $this->xpdo->getObject('sxNewsletter', array('id' => $entry['newsletter_id'], 'active' => 1))) {
            $newsletter->Subscribe($entry['user_id'], $entry['email']);
        }
        else {
            return false;
        }
    }
    else {
        return $this->Subscribe($entry['user_id'], $entry['email']);
    }
}

На странице может быть несколько сниппетов с подписками, поэтому я предусмотрел вызов нужного объекта sxNewsletter, иначе может быть путаница.

Subscribe

Метод для подписки пользователя на рассылку. Тут все просто: обычные проверки входящих данных и создание объекта:


$subscriber = $this->xpdo->newObject('sxSubscriber');
$subscriber->fromArray(array(
    'newsletter_id' => $this->id,
    'user_id' => $user_id,
    'email' => $email
), '', true, true);

Обратите внимание, что здесь не указывается поле новое code, его мы будем задавать при сохранении объекта sxSubscriber.

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

public function save($cacheFlag = null) {
    $hash = sha1(uniqid(sha1($this->user_id . $this->newsletter_id . $this->email), true));

    $this->set('code', $hash);
    return parent::save($cacheFlag);
}

Коммит файла sxsubscriber.class.php

unSubscribe

Метод для отписки читателя от рассылки. Получает code, ищет по нему запись в БД и удаляет.


if ($subscriber = $this->xpdo->getObject('sxSubscriber', array('code' => $code))) {
    return $subscriber->remove();
}

Пользователю не нужно ничего подтверждать. Если у него есть уникальный код для отписки - он отписывается в одно движение.

Вот коммит со всеми методами. Теперь нужно доработать сниппет Sendex, чтобы он мог их вызывать.

Работа с подпиской в сниппете

Сниппет, как и прежде, показывает разные формы, в зависимости от статуса пользователя. Но теперь он еще и случает в переменную sx_action массива $_REQUEST.

Если такая переменная есть, значит пользователь отправил форму, и нужно её обработать. Всего я предусмотрел 3 действия.

subscribeр4> Проверяем email и вызываем метод checkEmail из объекта подписки. Если в ответ приходит true, значит юзер уже подписан, если false - ошибка, в противном случае мы получаем уникальный ключ для доступа к данным в modRegistry.

Нужно отправить его письмом юзеру, и для этого я добавил метод Sendex::sendEmail(). Для оформления письма с активацией используется новый чанк tpl.Sendex.subscribe.activate

confirm

Юзер получает письмо и должен пройти по ссылке. В ней, конечно, указан другой sx_action - confirm. Для него сниппет вызывает sxNewsletter::confirmEmail() и передаёт туда хэш из ссылки в письме.

Если хэш верный, то метод получит данные и создаст нового подписчика sxSubscriber. При сохранении, как мы помним, ему будет сгенерирован уникальный код отписки.

unsubscribe

Если пользователь уже подписан на рассылку, ему показывается форма отписки и в неё я добавил скрытый input code. Если юзер отправит эту форму с sx_action = ubsubscribe, то код пойдёт в метод sxNewsletter::unSubscribe() и юзер будет отписан.

Код генерируется случайно, алгоритм sha1 практически исключает коллизии, так что отписан будет именно тот, кто нажал форму.

Еще пара замечаний

Во время работы сниппета могут проиходить разные нештатные ситуации. Юзер уже подписан, meail неверный и т.д. Так вот, если есть сообщение, то оно сохраняется в плейхолдер [[+message]], а если это ошибка, то выставляется и плейхолдер error.

Если же всё в порядке, то после обработки sx_action страница перезагружается, чтобы удалить данные из $_POST. Иначе, при нажатии F5 брайзер будет отправлять форму повторно.

Сниппет далек от идеала, и пока представлен в няглядной форме, для обучения. По хорошему, его нужно бы научить работать через ajax и более понятно выводить ошибки.

Но для наших целей и этого достаточно.

Вот коммит со всеми изменениями сниппета, чанков и лексиконов. Ах да, вот еще лексиконы.

Заключение

Итак, у нас есть сниппет, выводящий 2 формы, в зависимости от статуса юзера.

При отправке этих форм он вызывает методы в объекте рассылки и подписывает\отписывает юзеров. При подписке проверяется email, при отписке - нет.

Как только пользователь подписался, можно генерирвоать письма через админку или api и отпарвлять. На следующем занятии мы набросаем скрипты для автоматической постановки писем в очередь при разных событиях, и отправки по cron.

После этого, я думаю, вы позадаёте мне вопросы, и наши занятия закончатся, потому что первая бета-версия компонента будет готова.

← Предыдущая заметка
Сниппет Sendex и формы подписки\\отписки
Следующая заметка →
Рассылка по расписанию
Комментарии (6)
bazmasterВасилий Столейков
21.01.2014 08:18

В идеале, было бы здорово добавить сниппету ещё один параметр, что-то типа &uidAuth (user id), для того, чтобы подписать/отписать какого-то отдельного пользователя или сразу нескольких. Чтобы не делать это из админки одиночным добавлением. Пример:


[[!Sendex? 
    &id=`3` 
    &uidAuth=`1,5,6,8,169,589,1272,1565`
]]

Для чего это нужно: всех существующих пользователей подписать на новости сайта. Также в этот параметр может подставляться ID какого-то конкретного пользователя в чанке pdoUsers, чтобы администратор мог лично и быстро подписать нужных пользователей. P.S. Хотя, может я чего-то ещё не понимаю...

bezumkinВасилий Наумкин
21.01.2014 10:43

Сниппет вызывается на сайте самим пользователем и работает именно с ним. Других пользователей он трогать не должен.

Для административных нужд у нас есть методы Subscribe и unSubscribe.

bazmasterВасилий Столейков
21.01.2014 10:45

Логично, спасибо.

Алексей Карташов
30.01.2014 16:20

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

bezumkinВасилий Наумкин
30.01.2014 16:32

Ты видел, какой там код генерируется? 40 символов, фиг переберешь.

Алексей Карташов
30.01.2014 16:43

Ну в принципе, количество всех возможных вариантов равно 36^40 (0-9a-z = 36), что равно примерно 1786899102460170545314324772894400000000000000000000000000000000000000000000000000000000000000. Каковы там скорости перебора современных GPU?))

p.s. но проверочку всё-таки добавил :-) Примешь?

bezumkin
Василий Наумкин
04.07.2022 23:34
Что-то странное у тебя произошло: миграция есть, и вроде как выполнена, но таблицы при этом отсутств...
inetlover
Александр Наумов
03.07.2022 20:36
Василий, спасибо! Все понятно!
bezumkin
Василий Наумкин
02.07.2022 20:28
Спасибо, поправил!
bezumkin
Василий Наумкин
30.06.2022 03:58
Есть ли возможность формировать &quot;friendly URL aliases&quot;, используя аналог translit MODx? ...
bezumkin
Василий Наумкин
27.06.2022 03:32
Спасибо за исправления, очень выручаешь =) Но учитывая количество не описаных в заметке дополнительн...
bezumkin
Василий Наумкин
27.06.2022 03:10
что будет использоваться для вывода многоуровневого меню Посмотри как работают комментарии на этом ...
bezumkin
Василий Наумкин
25.06.2022 11:56
Поправил, спасибо!
bezumkin
Василий Наумкин
21.06.2022 01:58
onLoad(data) { this.total = data.total }, и onLoad({total}) { this.total = total }, В нашем случ...
bezumkin
Василий Наумкин
20.06.2022 14:01
Прекрасно тебя понимаю, я когда сам в этом разбирался - голова дымилась. Но зато теперь прямо-таки п...
bezumkin
Василий Наумкин
20.06.2022 09:30
Не надо, оно по умолчанию так - я просто чуть более подробно написал.