Интеграция PHPList и CakePHP для e-mail рассылок

December 13, 2008 | By admin | Filed in: CakePHP, Статьи.

Представим себе следующую ситуацию. После регистрации на сайте у пользователя есть возможность в настройках профиля поставить галочку “Хочу получать по e-mail новости проекта”, а у владельца сайта должна быть возможность эти новости всем подписавшимся пользователям рассылать при их появлении.

В CakePHP есть EmailComponent для отправки сообщений электронной почты, но, к сожалению, для рассылки этот компонент не подойдет. Если в базе будет много пользователей, то отправка им сообщений в цикле скорее всего приведет к таймауту скрипта.

Чтобы не писать свое решение с очередями сообщений, обходом тайм-лимита и всем прочим, решил посмотреть, что есть уже готового.

Бесплатных средств на PHP для проведения рассылок существует не так уж и много. Самым навороченным является PHPList (хотя, временами – не самым приятным в работе). А поскольку синхронизировать список подписанных пользователей сайта с таковым же в PHPList вручную представляется довольно неудобным, я решил это автоматизировать.

Итак, подробности нашей задачи:

  1. Есть модель UserSetting, которая содержит настройки пользователя
  2. У данной модели есть поле ‘get_site_emails’, в котором хранится либо 0, либо 1, в зависимости от желания пользователя получать наш “спам”
  3. При сохранении настроек пользователь должен автоматически добавляться в/удаляться из списка рассылки PHPList
  4. Пользователи бывают различных типов, для каждого типа в PHPList должен быть свой список рассылки

С точки зрения автоматизации данного процесса, в структуре БД PHPList нас интересуют три таблицы:

  1. phplist_user_user – таблица, содержащая информацию о пользователях
  2. phplist_list – таблица, содержащая информацию о списках рассылки
  3. phplist_listuser – таблица-связка между пользователями и списками (HABTM-ассоциация, если говорить на языке кейка). Для этой таблицы, кстати, модель нам не понадобится.

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

Первое, что нам нужно сделать – это создать дополнительную конфигурацию для подключения к БД PHPList. Данная операция не требуется, если таблицы PHPList находятся в основной БД вашего приложения. Итак, в файле “app\config\database.php” к описанию класса DATABASE_CONFIG добавляем следующее:

var $phplist = array(
        'driver' => 'mysql',
        'persistent' => false,
        'host' => 'localhost',
        'port' => '',
        'login' => 'root',
        'password' => '',
        'database' => 'php_list',
        'schema' => '',
        'prefix' => 'phplist_',
        'encoding' => 'utf8'
    );

Здесь ничего особенного. Значение ‘prefix’ в массиве указывает, что имена таблиц в БД содержат такой префикс. Далее в моделях нам нужно будет указывать только оставшуюся часть названия таблицы.

Идем дальше. Теперь нужно создать модели для вышеупомянутых таблиц PHPList. Для удобства создадим папку “app\models\phplist\”, в которой они и будут храниться.

Первая модель (без которой вполне можно обойтись 🙂 ) – php_list.php

<?php
class PhpList extends AppModel {

    var $name = 'PhpList';
    var $useDbConfig = 'phplist';
    var $recursive = -1;
}
?>

Это просто родительский класс для остальных моделей, чтобы в каждой из них не нужно было указывать повторяющиеся значения $useDbConfig. Данная переменная, кстати, указывает, что при подключении к БД нужно использовать не дефолтный конфиг, а phplist.

Вторая модель (списки рассылок) объемами кода также не отличается:

<?php
App::import('Model', 'PhpList');
class PhpListList extends PhpList {

    var $name = 'PhpListList';
    var $useTable = 'list';
}
?>

Здесь мы указываем, какую таблицу использовать. Как я и говорил, мы указываем название таблицы без префикса, т.к. он определен в конфиге.

Основная логика находится в модели PhpListUserUser:

<?php
App::import('Model', 'PhpList');
class PhpListUserUser extends PhpList {

    var $name = 'PhpListUserUser';
    var $useTable = 'user_user';

    var $hasAndBelongsToMany = array(
            'PhpListList' => array('className' => 'PhpListList',
                        'joinTable' => 'listuser',
                        'foreignKey' => 'userid',
                        'associationForeignKey' => 'listid',
                                                'with' => 'ListUsers'
                        ),
    );

    function addToList($email, $list) {
                //get list id
                $list_id = null;
        if(is_numeric($list)) {
            $list_id = $list;
        } elseif(is_string($list)) {
            if(!$_list = $this->PhpListList->find('first', array('fields' => array('id'), 'conditions' => array('name' => $list)))) {
            //list does not exist, create new
                $this->PhpListList->create(array('name' => $list, 'active' => 1));
                $this->PhpListList->save();
                $list_id = $this->PhpListList->id;
            } else {
                $list_id = $_list['PhpListList']['id'];
            }
        } else {
            return false;
        }

                //get user id
                $user_id = null;
                if($user = $this->find('first', array('fields' => array('id'), 'conditions' => array('email' => $email)))) {
                //existing user email
                        $user_id = $user['PhpListUserUser']['id'];
                        //check for existence of user in a list
                        if($this->ListUsers->find('count', array('conditions' => array('userid' => $user_id, 'listid' => $list_id)))) {
                                return true;
                        }
                } else {
                //new user email
                        $this->create(array('email' => $email, 'confirmed' => 1, 'htmlemail' => 1));
                        $this->save();
                        $user_id = $this->id;
                }

                //this is needed to bypass exists() check that will fail because of composite key
                $this->ListUsers->id = false;
                //finally add a user to list
                return $this->ListUsers->save(array('userid' => $user_id, 'listid' => $list_id));
    }

    function removeFromList($email, $list) {
        $conditions = null;
        if(is_numeric($list)) {
            $conditions = array('id' => $list);
        } elseif(is_string($list)) {
            $conditions = array('name' => $list);
        } else {
            return false;
        }

        if(!$_list = $this->PhpListList->find('first', array('fields' => array('id'), 'conditions' => $conditions))) {
                        return true;
                }
                if(!$user = $this->find('first', array('fields' => array('id'), 'conditions' => array('email' => $email)))) {
                        return true;
                }
                //simple delete query because of composite key in a table
                return $this->query("DELETE FROM ".$this->tablePrefix.$this->hasAndBelongsToMany['PhpListList']['joinTable']." WHERE listid = ".$_list['PhpListList']['id']." AND userid = ".$user['PhpListUserUser']['id']);
    }

}
?>

Что происходит в данном скрипте:

1. Определяется отношение hasAndBelongsToMany между моделями PhpListUserUser и PhpListList. Мы явно указываем название таблицы и ключей, т.к. соглашениям CakePHP они не соответствуют. Также при помощи with задаем название для “модели-связки”, которая будет автоматически создана в виде $this->ListUsers.

2. Функция addToList($email, $list) добавляет пользователя в список рассылки. Если в $list передается число, то считается, что список уже существует, и $list – его id. Если передается строка, то переданный параметр считается названием списка. В случае, если такого списка еще нет, он будет создан. Примерно то же самое с адресом пользователя, передаваемом в $email. Далее, если пользователя нет в нужном списке, мы его туда добавляем при помощи $this->ListUsers->save. Обратите внимание на строчку

$this->ListUsers->id = false;

перед вызовом метода сохранения данных. Если ее убрать, все будет работать, но вызов $this->exists() в методе save() сгенерирует неправильный запрос к БД, который получается за счет использования составного ключа в таблице-связке (а точнее, из-за невозможности определить primaryKey).

3. Функция removeFromList($email, $list) делает обратную операцию. Здесь все просто, но опять же, из-за составного ключа мы не можем пользоваться стандартным методом $this->del($id), поэтому для удаления записи приходится использовать $this->query().

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

<?php
class UserSetting extends AppModel {

    var $name = 'UserSetting';
    var $primaryKey = 'user_id';

    var $belongsTo = array(
            'User' => array('className' => 'User',
                                'foreignKey' => 'user_id',
            )
    );

    function afterSave($created) {
        App::import('Model', 'PhpListUserUser');
        $listUser = new PhpListUserUser;

        if(!$user = $this->User->read(array('User.email', 'User.type'), $this->data['UserSetting']['user_id'])) {
            return;
        }
        if($this->data['UserSetting']['get_site_emails']) {
        //subscribe user to list
            $listUser->addToList($user['User']['email'], Inflector::pluralize($user['User']['type']));
        } else {
        //delete user from list
            $listUser->removeFromList($user['User']['email'], Inflector::pluralize($user['User']['type']));
        }
    }

}
?>

После сохранения данных модели мы, в зависимости от значения get_site_emails, добавляем или удаляем пользователя из списка. Имена для списков получаются путем перевода названия типа пользователя во множественное число.


Tags: , , ,

Leave a Reply