Сейчас мы создадим с вами интеграцию между такими сервисами как Trello и Todoist. Данная задача понадобилась в первую очередь мне самому, так как проектами все-же удобнее управлять в досках, а вот видеть текущий список задач и отмечать их выполнения нравится в Todoist.
Техническое задание
Основная задача состоит в том, чтобы все карточки, у которых задана дата выполнения, автоматически создавались как задачи в Todoist. Если в карточке есть чек-лист, он должен создаться подзадачами в основной задаче. Каждая задача должна помечаться меткой с названием доски к которой принадлежит. При выполнении задачи в Todoist соответствующая доска в Trello также помечается как выполненная. В случае переименования доски или карточки, или же изменения даты выполнения происходят соответствующие обновления и в Todoist.
| Trello | Todoist | Примечание |
| Новая доска. | Создается метка с соответствующим названием. | |
| Переименование доски. | Переименование метки. | |
| Карточка без даты выполнения. | — | |
| Карточка с датой выполнения. | Создается задача. | При выполнении задачи соответствующая карточка отмечается выполненной. |
| Карточка с датой выполнения и чек-листом. | Создается задача для карточки, как основная. Для пунктов чек-листа создаются вложенные задачи. | При выполнении подзадачи отмечается соответствующий пункт в чек-листе. |
| Переименование карточки или пункта чек-листа. | Переименование задачи. | |
| Изменение даты выполнения карточки. | Изменения даты выполнения задачи. |
Структура проекта
Для получения, создание и обновления данных будем использовать API соответствующих сервисов. Давайте выделим для себя основные сущности которые понадобятся в работе. Для Trello это доски (чтобы создавать метки), карточки и чек-листы (чтобы создавать задачи). Для Todoist это задачи и метки.
Создаем проект, структура
src/Integration.php src/Todoist.php src/Trello.php config.php index.php
База данных
Для отслеживания выполнения будем использовать промежуточную базу данных задач, состоящую из двух таблиц projects и tasks.
Projects — таблица соответствий между досками в Trello и метками Todoist
CREATE TABLE `projects` ( `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `trello_id` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL, `todoist_id` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Tasks — таблица обработанных задач
CREATE TABLE `tasks` ( `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `due_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `name` tinytext COLLATE utf8mb4_unicode_ci NOT NULL, `project_id` int(11) UNSIGNED NOT NULL DEFAULT 0, `trello_id` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `trello_status` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT 'incomplete', `trello_updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `trello_card_id` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `trello_checklist` tinyint(1) NOT NULL DEFAULT 0, `trello_checklist_id` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `todoist_id` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `todoist_status` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT 'incomplete', `todoist_updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Алгоритм работы
Класс Integration:
- подключить базу данных;
- создать проект;
- обновить проект;
- создать задачу;
- обновить задачу;
- получить проект с БД;
- получить задачу с БД;
- получить список невыполненных задач;
- отметить задачу как выполненную.
Класс Todoist:
- здесь соответственно нужны два метода get и post для получения и отправки данных;
- создать метку;
- обновить метку;
- создать задачу;
- обновить задачу;
- получить список всех задач;
- проверить задачу, выполненная или нет, данный метод нужен, так как в API нету возможности получить информацию об одной конкретной задаче.
Класс Trello:
- методы get и put, здесь в API используется для обновления метод PUT.
- получить все доски;
- получить все карточки, которые удовлетворяют условию задачи;
- получить список пунктов из чек-листа;
- изменить состояние пункта чек-листа на выполнено;
- изменить статус задачи на выполнено.
Структура index.php:
- обновления статуса задач, для этого делаем выборку всех невыполненных задач с базы и всех активных задач в Todoist. Проходим циклом по всем выполненным задач, если какой-то нету в списке активных, значит она выполнена. Обновляем статус в промежуточной БД и Trello.
- делаем выборку всех досок Trello, проходимся по каждой, если такой доски нету в БД, добавляем и создаем соответствующую метку в Todoist. Если есть, проверяем не была ли она переименованная.
- делаем выборку всех карточек из текущей доски. Проходим циклом по ним, проверяем наличие задачи в БД, если нету — создаем в БД и Todoist, если есть проверяем название задачи.
- проверяем наличие чек-листа в текущей карточке, если такой есть, получаем список его невыполненных пунктов и создаем соответствующие подзадачи.
Разбор кода
1. config.php — файл конфигурации.
$config = [
// Ключ и токен для работы с API Trello
'trello' => [
'key' => '',
'token' => '',
],
// Токен для работы с API Trello
'todoist' => [
'token' => '',
],
// Данные для подключения к БД MySQL
'db' => [
'host' => '',
'name' => '',
'user' => '',
'password' => '',
'charset' => '',
],
];
Ключи и тестовые токены можно получить перейдя по соответствующим ссылкам из документации:
REST API Todoist — https://developer.todoist.com/rest/v1/#overview
REST API Trello — https://developer.atlassian.com/cloud/trello/rest/api-group-actions/
2. src/Integration.php — набор методов для работы с промежуточной БД
<?php
class Integration
{
/*
* @var new PDO
*/
private $db = null;
/*
* @param array $config
* @return void
*/
public function __construct($config) {
// Создаем подулючение к БД
$db = new PDO ('mysql:host=' . $config['db']['host'] . ';dbname=' . $config['db']['name'], $config['db']['user'], $config['db']['password']);
$db->query('SET character_set_connection = ' . $config['db']['charset'] . ';');
$db->query('SET character_set_client = ' . $config['db']['charset'] . ';');
$db->query('SET character_set_results = ' . $config['db']['charset'] . ';');
$this->db = $db;
}
/*
* Создание проекта.
*
* @param array $data
* @return int PDO::lastInsertId()
*/
public function createProject($data)
{
$res = $this->db->prepare('INSERT IGNORE INTO projects (`created`, `name`, `trello_id`, `todoist_id`) VALUES (:created, :name, :trello_id, :todoist_id)');
$res->execute([
':created' => date('Y-m-d H:i:s'),
':name' => $data['name'],
':trello_id' => $data['board_id'],
':todoist_id' => $data['label_id'],
]);
return $this->db->lastInsertId();
}
/*
* Создание задачи.
*
* @param int $projectId
* @param string $taskId
* @param array $card
* @param null $checklistId
* @param array $checkItem
* @return void
*/
public function createTask($projectId, $taskId, $card, $checklistId, $checkItem)
{
$checklist = 0;
if ($checklistId != null) {
$checklist = 1;
}
$res = $this->db->prepare("INSERT IGNORE INTO tasks (`created`, `due_date`, `name`, `project_id`, `trello_id`, `trello_card_id`, `trello_checklist`, `trello_checklist_id`, `todoist_id`)
VALUES (:created, :due_date, :name, :project_id, :trello_id, :trello_card_id, :trello_checklist, :trello_checklist_id, :todoist_id)");
$res->execute([
':created' => date('Y-m-d H:i:s'),
':due_date' => date('Y-m-d H:i:s', strtotime($card['due'])),
':name' => $checkItem['name'],
':project_id' => $projectId,
':trello_id' => $checkItem['id'],
':trello_card_id' => $card['id'],
':trello_checklist' => $checklist,
':trello_checklist_id' => $checklistId,
':todoist_id' => $taskId,
]);
}
/*
* Обновление проекта.
*
* @param int $id
* @param string $name
* @return void
*/
public function updateProject($id, $name)
{
$res = $this->db->prepare("UPDATE projects SET `name` = :name WHERE id = :id");
$res->execute([
':name' => $name,
':id' => $id,
]);
}
/*
* Обновление задачи.
*
* @param int $id
* @param array $query
* @return void
*/
public function updateTask($id, $query)
{
$res = $this->db->prepare("UPDATE tasks SET `name` = :name, `due_date` = :due_date WHERE id = :id");
$res->execute([
':name' => $query['name'],
':due_date' => date('Y-m-d H:i:s', strtotime($query['due_date'])),
':id' => $id,
]);
}
/*
* Получение информации о проекте.
*
* @param string $id
* @return array
*/
public function getProject($id)
{
$res = $this->db->query("SELECT * FROM projects WHERE trello_id = '{$id}'");
return $res->fetch();
}
/*
* Получение информации о задаче.
*
* @param string $id
* @return array
*/
public function getTask($id)
{
$res = $this->db->query("SELECT * FROM tasks WHERE trello_id = '{$id}'");
return $res->fetch();
}
/*
* Получение списка невыполненных задач.
*
* @return array
*/
public function getIncompleteTasks()
{
$res = $this->db->query("SELECT * FROM tasks WHERE todoist_status = 'incomplete'");
return $res->fetchAll();
}
/*
* Отметка задачи выполненной.
*
* @param int $id
* @param string $date
* @return void
*/
public function setTaskComplete($id, $date)
{
$date = date('Y-m-d H:i:s', $date);
$res = $this->db->prepare("UPDATE tasks SET trello_status = :trello_status, trello_updated = :trello_updated, todoist_status = :todoist_status, todoist_updated = :todoist_updated WHERE id = :id");
$res->execute([
':trello_status' => 'complete',
':trello_updated' => $date,
':todoist_status' => 'complete',
':todoist_updated' => $date,
':id' => $id,
]);
}
}
3. src/Todoist.php — набор методов для работы с API Todoist
<?php
class Todoist
{
/*
* @var array $apiData
*/
private $apiData = [
'token' => '',
];
/*
* @param array $config
* @return void
*/
public function __construct($config)
{
$this->apiData = $config['todoist'];
}
/*
* @param string $url
* @return string $output
*/
public function get($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url . '?token=' . $this->apiData['token']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
curl_close($ch);
// обработка ошибок
return $output;
}
/*
* @param string $url
* @param string $query
* @return string $output
*/
public function post($url, $query)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url . '?token=' . $this->apiData['token']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $query);
$output = curl_exec($ch);
curl_close($ch);
// обработка ошибок
return $output;
}
/*
* Получение списка активных задач
*
* @return array $output
*/
public function getTasks()
{
$output = $this->get('https://api.todoist.com/rest/v1/tasks');
$output = json_decode($output);
return $output;
}
/*
* Проверка статуса задачи
*
* @param array $tasks
* @param string $taskId
* @return array
*/
public function getTaskCompleteDate($tasks, $taskId)
{
foreach ($tasks AS $task) {
if ($task->id == $taskId) {
return ['status' => 'incomplete'];
}
}
return ['status' => 'complete', 'date' => time()];
}
/*
* Создание задачи
*
* @param array $query
* @return array $output
*/
public function addTask($query)
{
$output = $this->post('https://api.todoist.com/rest/v1/tasks', json_encode($query, JSON_NUMERIC_CHECK));
$output = json_decode($output);
return (array) $output;
}
/*
* Создание метки
*
* @param array $query
* @return array $output
*/
public function addLabel($query)
{
$output = $this->post('https://api.todoist.com/rest/v1/labels', json_encode($query));
$output = json_decode($output);
return (array) $output;
}
/*
* Обновление задачи
*
* @param string $id
* @param array $query
* @return void
*/
public function updateTask($id, $query)
{
$this->post('https://api.todoist.com/rest/v1/tasks/' . $id, json_encode($query));
}
/*
* Обновление метки
*
* @param string $id
* @param array $query
* @return void
*/
public function updateLabel($id, $query)
{
$this->post('https://api.todoist.com/rest/v1/labels/' . $id, json_encode($query));
}
}
4. src/Trello.php — набор методов для работы с API Trello
<?php
class Trello
{
/*
* @var array $apiData
*/
private $apiData = [
'key' => '',
'token' => '',
];
/*
* @param array $config
* @return void
*/
public function __construct($config)
{
$this->apiData = $config['trello'];
}
/*
* @param string $url
* @return string $output
*/
public function get($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url . '?key=' . $this->apiData['key'] . '&token=' . $this->apiData['token']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
$output = curl_exec($ch);
curl_close($ch);
// обработка ошибок
return $output;
}
/*
* @param string $url
* @param string $query
* @return string $output
*/
public function put($url, $query)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url . '?key=' . $this->apiData['key'] . '&token=' . $this->apiData['token']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($query));
$output = curl_exec($ch);
curl_close($ch);
// обработка ошибок
return $output;
}
/*
* Получение списка досок
*
* @return array $boards
*/
public function getBoards()
{
$output = $this->get('https://api.trello.com/1/members/me/boards');
$output = json_decode($output);
$boards = [];
foreach ($output AS $board) {
if ($board->closed) {
continue;
}
$boards[] = [
'name' => $board->name,
'id' => $board->id,
];
}
return $boards;
}
/*
* Получение списка карточек из доски
*
* @param string $id
* @return array $cards
*/
public function getCards($id)
{
$output = $this->get('https://api.trello.com/1/boards/' . $id . '/cards');
$output = json_decode($output);
$cards = [];
foreach ($output AS $card) {
if ($card->closed || $card->dueComplete || empty($card->due)) {
continue;
}
$cards[] = (array) $card;
}
return $cards;
}
/*
* Получение списка пунктов чек-листа
*
* @param string $id
* @return array $output
*/
public function getChecklist($id)
{
$output = $this->get('https://api.trello.com/1/checklists/' . $id);
$output = json_decode($output);
return (array) $output;
}
/*
* Обновления пункта чек-листа, пометка как выполненного
*
* @param string $cardId
* @param string $checkitemId
* @return void
*/
public function setChecklistCompleteItem($cardId, $checkitemId)
{
$this->put('https://api.trello.com/1/cards/' . $cardId . '/checkItem/' . $checkitemId, ['state' => 'complete']);
}
/*
* Обновления карточки, пометка как выполненной
*
* @param string $cardId
* @return void
*/
public function setCardComplete($cardId)
{
$this->put('https://api.trello.com/1/cards/' . $cardId, ['dueComplete' => 1]);
}
}
5. index.php — основной исполняемый файл
<?php
include_once 'config.php';
include_once 'src/Integration.php';
include_once 'src/Trello.php';
include_once 'src/Todoist.php';
// Экземпляры классов для работы
$todoist = new Todoist($config);
$integration = new Integration($config);
$trello = new Trello($config);
// Проверка списка текущих задач и отметка выполнених
$tasks = $todoist->getTasks();
$incompleteTasks = $integration->getIncompleteTasks();
// Обход списка невыполненых задач для сравнения с текущими задачи в Todoist
foreach ($incompleteTasks AS $incompleteTask) {
$taskStatus = $todoist->getTaskCompleteDate($tasks, $incompleteTask['todoist_id']);
if ($taskStatus['status'] == 'complete') {
// Если задача является элементом чек-листа, отмечаем его выполненым, если нет - отмечаем выполненой карточку
if ($incompleteTask['trello_checklist'] == 1) {
$trello->setChecklistCompleteItem($incompleteTask['trello_card_id'], $incompleteTask['trello_id']);
} else {
$trello->setCardComplete($incompleteTask['trello_id']);
}
$integration->setTaskComplete($incompleteTask['id'], $taskStatus['date']);
}
}
// Делаем выборку всех досок для создания меток
$boards = $trello->getBoards();
foreach ($boards AS $board) {
// Проверяем наличие доски в БД
$project = $integration->getProject($board['id']);
if (!isset($project['id'])) {
$label = $todoist->addLabel(['name' => $board['name']]);
$labelId = $label['id'];
$projectId = $integration->createProject([
'name' => $board['name'],
'board_id' => $board['id'],
'label_id' => $labelId,
]);
} else {
$projectId = $project['id'];
$labelId = $project['todoist_id'];
// В случае изменения имени доски, перименовываем метку
if ($project['name'] != $board['name']) {
$integration->updateProject($projectId, $board['name']);
$todoist->updateLabel($labelId, ['name' => $board['name']]);
}
}
// Получаем список карточек в текущей доске, которые имеет дату выполнения
$cards = $trello->getCards($project['trello_id']);
foreach ($cards AS $card) {
$task = $integration->getTask($card['id']);
if (!isset($task['id'])) {
$task = $todoist->addTask([
'content' => $card['name'],
'label_ids' => [$labelId],
'due_datetime' => $card['due'],
]);
$taskId = $task['id'];
$integration->createTask($projectId, $taskId, $card, null, $card);
} else {
$taksId = $task['todoist_id'];
// В случае изменения карточки (имя или дата), редактируем текущую задачу
if ($task['name'] != $card['name'] || $task['due_date'] != date('Y-m-d H:i:s', strtotime($card['due']))) {
$integration->updateTask($task['id'], ['name' => $card['name'], 'due_date' => $card['due']]);
$todoist->updateTask($taksId, ['content' => $card['name'], 'due_datetime' => $card['due']]);
}
}
// Проверяем наличие чек-листа у карточки
if (!empty($card['idChecklists'])) {
// Обходим все чек-листы
foreach ($card['idChecklists'] AS $checklistId) {
$checkItems = $trello->getChecklist($checklistId);
foreach ($checkItems['checkItems'] AS $checkItem) {
$checkItem = (array) $checkItem;
$task = $integration->getTask($checkItem['id']);
if (!isset($task['id']) && $checkItem['state'] == 'incomplete') {
$task = $todoist->addTask([
'content' => $checkItem['name'],
'label_ids' => [$labelId],
'due_datetime' => $card['due'],
'parent' => $taskId,
]);
$integration->createTask($projectId, $task['id'], $card, $checklistId, $checkItem);
} else {
// В случае изменения пунтка чек-листа (имя) или карточки (дата), редактируем текущую задачу
if ($task['name'] != $checkItem['name'] || $task['due_date'] != date('Y-m-d H:i:s', strtotime($card['due']))) {
$integration->updateTask($task['id'], ['name' => $checkItem['name'], 'due_date' => $card['due']]);
$todoist->updateTask($task['todoist_id'], ['content' => $checkItem['name'], 'due_datetime' => $card['due']]);
}
}
}
}
}
}
}
На этом все, скрипт готов (скачать на github), остается установить код на сервер и добавить задачу на выполнение файла index.php в планировщик cron. Интервал можно выставить каждую минуту, если досок и задач очень много, лучше поставить 2-3 минуты. Пример задачи в планировщике:
* * * * * php /path/to/index.php