Это я, почтальон Печкин, принес заметку… Работа с API MailChimp
Вступление
Когда-то давно мы всем офисом sawtech выехали на хакатон. По итогам этого действа с моей стороны был создан сайт размещения вакансий AppJobs. Мне было за него немного стыдно, он был немного корявый, неоптимизированный, с плохой структуруой, его по идее нужно было писать либо на frameworke, либо на голом php, но я умел только wordpress. Но во-первых, если ты вышел на рынок с продуктом, за который тебе не стыдно, значит ты поздно вышел на рынок (мопед не мой, я только объяву дал. Кому принадлежит эта цитата не помню), А во-вторых, сейчас я уже не комплексую по поводу WP, если ты хорошо умеешь работать топором, и с его помощью сможешь вырезать скульптуру, то почему бы и нет?
Хакатон был «заказной», всё что там создавалось, создавалось не совсем для себя, а для нашего хорошего знакомого и партнера. В общем, сайтик был сделан, мы его потом еще немого допиливали, а потом благополучно забили. По размещенным вакансиям видно, что он не очень живой, хотя какие-то люди что-то на нем размещали.
И вот с месяц назад, наш старый друг пишет письмо: «А давайте мы его реанимируем, и начнем с email подписки на вакансии?» Почему бы, собственно, и нет?
Постановка задачи
Забегая вперед: всегда, всегда(!) составляй письменное ТЗ до того как начал работать, даже с друзьями, даже если работаешь с мамой. Чтобы потом не случилось «ой». В данном случае ТЗ я получил в следующем виде: нужно сделать подписку на новые вакансии, чтобы когда добавиляется вакансия, автоматически уходила рассылка. К такому заданию была приложена картинка
Что я понял из постановки задачи:
- Нужна форма подписки на email рассылку
- нужна реализация самой рассылки
- Подписка пользователей
- Возможность отписаться
- Собственно сами email сообщения
Вот как-то так.
Реализация
Т.к. из предпочтений заказчик высказал варианты использования MailGun и MailChimp, то я сразу пошел гуглить API и того и другого. Вариант MailGun мне не нравился, потому, на сколько я понял, сервис предоставляет шлюз рассылки большого объема сообщений, это хорошо, но при этом всю процедуру подписки, отписки, и управления подписчиками, придется делать самому — это минус.
API MailChimp мне показалось мудреным и потратив пару дней я почти было сдался, но одним прекрасным утром меня пробило и я всё понял. Проблема была не в документации, а в моем понимании предметной области и механизмов работы сервиса.
Поясню. Я думал, что в mailchimp есть листы рассылки, и есть письма. Можно зайти в лист и создать для него новый email, либо наоборот, создаем email и указываем ему по какому листу рассылаться. А со стороны API я это представлял примерно так:
- добавление пользователей — это один запрос (так оно и оказалось).
- новая email рассылка — тоже один запрос, набрал email, в запросе указал с каким листом работать и отправил на сервер mailchimpa.
Со вторым я крупно ошибся, и по этой причине долго ковырялся. В понятиях MailChimp есть такая штука как Campaigns (компания), типа рассылочной компании. У меня почему-то ассоциации с предвыборной или агитационной компанией =) Таким образом сначала я должен создать компанию, выбрать кучу параметров, в том числе шаблон письма, с каким листом оно будет работать, и только после всех настроек можно запустить компанию в работу, т.е. разослать email’ы.
Но пойдем по порядку. Чтобы работать с API нужно получить ключ доступа, сейчас это находится в разделе Profile → Extras → API keys, к тому же, из особенностей, все запросы отправляются не на единый url, а на определенные поддомены значение которых соответствует поддомену админ-панели, проще картинкой объяснить
А адрес запросов равен https://<dc>.api.mailchimp.com/3.0/, где вместо <dc> у меня будет us15.
Подписать человека на рассылку, простейший вариант:
/** * Добаление подписчика * * @param string $list * @param string $email * @param string $name */ function addSubscriber($list = '', $email = '', $fname = '', $lname = '') { $data = array( 'email_address' => $email, 'status' => 'pending', 'merge_fields' => array('FNAME' => $fname, 'LNAME' => $lname) ); $res = $this->request('lists/' . $list . '/members', 'post', $data); $return = 'На указанную почту придет письмо с подтвержением подписки.'; if ($res->status == 400) { switch ($res->title) { case 'Member Exists': $return = 'Вы уже подписались ранее'; break; default: $return = $res->title; break; } } return $return; }
Прикольный момент, если в массиве параметров в status передать значение не pending, а subscribed, то пользователь будет подписан без дополнительного подтверждения.
А теперь опишу функции, которые умеют создавать компанию, и отправлять рассылки
<?php /** * Создание компании * * @param string $list_id * @param string $subj * @param string $from_name * @param string $reply_to * * @return mixed */ public function createCamping($list_id = '', $subj, $from_name, $reply_to) { $data = array( 'type' => 'regular', 'recipients' => array('list_id' => $list_id), 'settings' => array( 'subject_line' => $subj, 'reply_to' => $reply_to, 'from_name' => $from_name ) ); $res = $this->request('campaigns', 'post', $data); return $res; } /** * Создание текста для компании * * @param $html * @param $id_camping * * @return mixed */ public function createCampingContent($plain, $html, $id_camping) { $data = array('plain_text' => $plain, 'html' => $html); $res = $this->request('campaigns/' . $id_camping . '/content', 'put', $data); return $res; } /** * Отправка компании * * @param $id_camping id компани */ public function sendCamping($id_camping = '') { $res = $this->request('campaigns/' . $id_camping . '/actions/send', 'post'); return $res; }
Из вышеописанных функций можно сделать вывод, что при создании компании нельзя сразу создать контент для неё, по крайней мере, я не нашел этого в документации. Поэтому нужно отдельным запросом отправлять контент. Кстати, если попробовать изменить уже разосланную компанию, то вернется ошибка, т.к. политика MailChimp этого не позволяет, что мне показалось странно. Из этого же следует, что каждая новая рассылка будет создавать новую компанию. Вот такая вот странная политика.
Работать эти функции будут примерно так
$res = createCamping($list_id, 'Новая вакансия на AppJobs.ru', 'AppJobs', 'from@email.domain'); // Получим id новой компании $camp_id = $res->id; // Добавлем к ней контент $res = createCampingContent($plain, $html, $camp_id); // Отправка рассылки $res = $chimp->sendCamping($camp_id);
Я, будучи крутым веб-разработчиком, естественно пожелал обернуть всё это в ООП вид, в итоге у меня получился такой класс:
<?php /** * Класс для работы с АПИ * * Class AppjobsMailchimp */ class AppjobsMailchimp { /** * АПИ ключь * * @var */ protected $api_key; /** * Адрес запроса к апи * * @var string */ protected $url = 'https://<dc>.api.mailchimp.com/3.0/'; /** * AppjobsMailchimp constructor. * * @param $key * @param $dc */ public function __construct($key, $dc) { $this->api_key = $key; $this->url = str_replace('<dc>', $dc, $this->url); } /** * Выполенение запроса к АПИ * * @param string $method метод запроса * @param array|bool $data Данные * * @return mixed */ protected function request($method = '', $type = 'post', $data = false) { $url = $this->url . $method; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HTTPHEADER, array( 'Accept: application/vnd.api+json', 'Content-Type: application/vnd.api+json', 'Authorization: apikey ' . $this->api_key )); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); switch ($type) { case 'post': curl_setopt($ch, CURLOPT_POST, true); if (is_array($data)) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } break; case 'get': $query = http_build_query($data, '', '&'); curl_setopt($ch, CURLOPT_URL, $url . '?' . $query); break; case 'delete': curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); break; case 'patch': curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH'); if (is_array($data)) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } break; case 'put': curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); if (is_array($data)) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } break; } $out = curl_exec($ch); curl_close($ch); return json_decode($out); } /** * Получить список доступных листов рассылки * * @return array|mixed|object */ public function getLists() { return $this->request('lists', 'get'); } /** * Добаление подписчика * * @param string $list * @param string $email * @param string $name */ public function addSubscriber($list = '', $email = '', $name = '') { $data = array( 'email_address' => $email, 'status' => 'pending', 'merge_fields' => array('FNAME' => $name, 'LNAME' => '') ); $res = $this->request('lists/' . $list . '/members', 'post', $data); $return = 'На указанную почту придет письмо с подтвержением подписки.'; if ($res->status == 400) { switch ($res->title) { case 'Member Exists': $return = 'Вы уже подписались ранее'; break; default: $return = $res->title; break; } } return $return; } /** * Списко компаний * * @return mixed */ public function getCampaigns() { $res = $this->request('campaigns', 'get'); return $res; } /** * Создание компании * * @param string $list_id * @param string $subj * @param string $from_name * @param string $reply_to * * @return mixed */ public function createCamping($list_id = '', $subj, $from_name, $reply_to) { $data = array( 'type' => 'regular', 'recipients' => array('list_id' => $list_id), 'settings' => array( 'subject_line' => $subj, 'reply_to' => $reply_to, 'from_name' => $from_name ) ); $res = $this->request('campaigns', 'post', $data); return $res; } /** * Создание текста для компании * * @param $html * @param $id_camping * * @return mixed */ public function createCampingContent($plain, $html, $id_camping) { $data = array('plain_text' => $plain, 'html' => $html); $res = $this->request('campaigns/' . $id_camping . '/content', 'put', $data); return $res; } /** * Отправка тестового письма для компании * * @param string $id_camping компании * @param string $email куда отправлять тестовое сообщение * * @return mixed */ public function testCamping($id_camping, $email = '') { $data = array('test_emails' => array($email), 'send_type' => 'html'); $res = $this->request('campaigns/' . $id_camping . '/actions/test', 'post', $data); return $res; } /** * Отправка компании * * @param $id_camping id компани */ public function sendCamping($id_camping = '') { $res = $this->request('campaigns/' . $id_camping . '/actions/send', 'post'); return $res; } /** * Получить список доступных шаблонов * * @return mixed */ public function getTemplates() { $res = $this->request('templates/', 'get'); return $res; } }
Для WP я еще сделал обертку в виде плагина, который использует созданный класс. Пока не буду приводить код, т.к. он заточен под конкретный сайт, когда-нибудь потом я его пересоберу в более универсальный вариант и выложу.
Итог
Поставленную задачу я решил, но есть одно но. Когда я отправил заказчику результат работы, он сказал так: «Круто! Только я хотел, чтобы можно было подписываться на конкретный раздел вакансий, а не на любую. Потому что дизайнеры не хотят получать вакансии разработчика». Дальше были мои непечатные мысли. Но я уже знаю, что у MailChimp есть какая-то штука — сегменты. Вот в сторону сегментов я и буду дальше рыть. Не переключайтесь, после рекламы мы продолжим!
Всем рок!