Сейчас мы создадим с вами интеграцию между такими сервисами как 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