Интеграция Trello и Todoist. Скрипт PHP, работа с API

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