toggl→Redmine時間データ反映ツール

togglで勤怠管理、Redmineのチケットに作業時間を付ける運用時、両者の合計は一致するはずだが、手入力運用では面倒。

togglデータにRedmineのチケット情報を持たせて、Redmineへ時間データを付けられたら必ず一致して手間が省けて超便利です。

ということで各APIを呼び出し、処理するツールを作成しました。

フロー

前提

togglのdescriptionに、所定の形式でRedmineのチケット番号を書く

(例)黄色部分。チケット番号 12533

コード

※細かいエラー処理は省いています。

※実際のコードから抜粋しているため、修正ミスがある(このままでは動かない)かもしれません。あくまでざっとした流れの参考として見てください。

toggl APIを利用してデータ取得するメソッド

ToggleRepository.php

    /**
     * Toggglの作業記録を取得する
     *
     * @param string $apiToken
     * @param string $startDate=''
     * @param string $endDate=''
     * @return mixed
     *
     */
    public function getEntries(
        string $apiToken,
        string $startDate='',
        string $endDate=''
    ): mixed
    {
        $togglUri = 'https://api.track.toggl.com/api/v9/me/time_entries';  // 実際はconfig等に設定しておく
        $options = ['auth' => [$apiToken, 'api_token']];  // 取得対象のユーザのapi tokenをセットする

        $query = http_build_query([
            'start_date' => $startDate,
            'end_date' => $endDate,
        ]);
        $entryRequest = (new \GuzzleHttp\Client())->request('GET', "{$togglUri}?{$query}", $options);

        $res = $entryRequest->getBody()->getContents();
        return json_decode($res, true);
    }

Redmine APIを利用してデータ登録するメソッド

    /**
     * Redmineに時間エントリを登録する
     *
     * @param array $xmlValues
     * @return bool
     *
     */
    public function addTimeEntry(string $apiToken, array $xmlValues) : bool
    {
        $xml = new \SimpleXMLElement('<time_entry></time_entry>');
        foreach ($xmlValues as $key => $value) {
            $xml->addChild($key, (string)$value);
        }

        //Redmine に作業時間を登録する
        $redmineUri = self::REDMINE_URL . "time_entries.xml?key={$apiToken}";
        $request = (new \GuzzleHttp\Client())->request('POST', $redmineUri, [
                'headers' => ['Content-Type' => 'text/xml',],
                'body' => $xml->asXML()
        ]);

        return true;
    }

メイン処理

    /**
     * main処理
     * @return array
     */
    private function exec() :array
    {
        $this->togglApiToken = 'xxxxxxxxxxxxx' // 取得対象のユーザのapi token
        $this->redmineApiKey = 'ZZZZZZZZZZZZZ'; // 対象ユーザのRedmineのAPIキー
        $this->redmineUserId = 0; // 対象ユーザのRedmineのユーザID

        // toggl : 作業記録を取得する
        $entries = $this->toggleRepository->getEntries(
            $this->togglApiToken,
            $this->dateTime->setTime(00, 00, 00)->format('c'),
            $this->dateTimeEnd->setTime(23, 59, 59)->format('c')
        );

        // toggl の作業内容から Redmine用の作業時間データを作成し登録する
        return $this->makeRecord($entries);
    }
    /**
     * Toggl の作業内容から Redmine用の作業時間データを作成し登録する
     *
     * @param array $entries
     * @param array $redmineTimeRecs
     * @return array : 実行結果
     *
     */
    private function makeRecord(array $entries, array $redmineTimeRecs) : array
    {
        foreach ($entries as $entry) {
            $description = '';

            // 作業時間の時間
            // 開始・終了の差分
            $start = new \DateTime($entry['start']);
            $start->setTimezone($this->timezone);

            if (!isset($entry['stop'])) {
                $this->results->addIssueSkipNowExec($start, $description, 'timer 実行中です');
                continue;
            }

            $stop = new \DateTime($entry['stop']);
            $stop->setTimezone($this->timezone);

            $diff = $stop->diff($start);
            $min = round(($diff->i / 60), 3);
            $diffHours = $diff->h + $min;

            // Redmine でタイマーを打ったときのフォーマットに沿って` #1234 `の文字列をチケットIDとして検索
            preg_match('/\#([0-9]+)/', $description, $preg_match_results);

            //Redmine に作業時間を登録する
            $ret = $this->addToRedmine($issueId, $entry, $start, $diffHours, $description);
            if ($ret) {
                // 成功時処理
            } else {
                // エラー時処理
            }
        }

        return [
            // 結果
        ];
    }
    /**
     * Redmine に作業時間を登録する
     *
     * @param int $issueId
     * @param array $entry
     * @param Datetime $start
     * @param float $diffHours
     * @param string $description
     * @return bool
     *
     */
    private function addToRedmine (
        int $issueId,
        array $entry,
        Datetime $start,
        float $diffHours,
        string $description
    ) : bool
    {
        $xmlValues = [
            'issue_id' => $issueId,
            'spent_on' => $start->format('Y-m-d'),
            'hours' => $diffHours,
            'activity_id' => 8,  // $entry['tags']からタグが取得できるので、必要ならRedmineのタグのIDと対応させてセットする
            'user_id' => $this->redmineUserId, // 対象ユーザのRedmineのユーザID
            'comments' => $description,
        ];

        return $this->redmineRepository->addTimeEntry($this->redmineApiKey, $xmlValues);
    }

実行結果

Redmine、赤部分が追加されている。

Note

実際には、以下の処理に対応しています

・すでにredmineに時間登録済みか?重複チェック
・redmineに該当番号のチケットがあるか?チェック
・該当番号のチケットにそのユーザが権限を持っているか?チェック