Google Calendar API v3でスケジュール同期 OAuth対応版


# はじめに

以前の投稿（[Google Calendar API v3でスケジュール同期](http://qiita.com/qoAop/items/9f4d08ec7ede42849b41)）では.p12認証キーを使っていたんですが、ライブラリもバージョンアップしてOAuthが前提になっていました。
仕様変えるなよ...って感じですよね...

...ということで、OAuthに対応したGoogle Calendar API v3に対応した同期ツールをphpで作りました。

私は同期したいデータを落としてきてperlでdata.csvの形に整形し、その後cal.phpを実行するようにcronで設定してます。

# Google Developer Consoleでのアプリケーション設定
まず、[Google Developer Console](https://console.developers.google.com/project)にいってアプリケーションのプロジェクトを作成してください。

細かい操作は[このページ（Developers.io）が詳しくておすすめ](http://dev.classmethod.jp/cloud/google-calendar-api-get-start/)です。
ダウンロードした【client_secret_NNNNNNNNNN-XXXXXX.apps.googleusercontent.com】というファイル名を【client_secret.json】に変更しておいてください。

# Google Calendarの設定

次は[Google Calendar](https://calendar.google.com/)の方に同期専用カレンダーを作成します。

1. マイカレンダーで同期したいカレンダーを作成してください。※普段使ってるのとは別に同期専用カレンダーを作成してください。
2. 【カレンダー名】をメモしておいてください。

以前はここでいろいろ設定しましたが、新しいのはOAuthの権限認可画面で行うので特に必要はないようです。
これでGoogle Calendar側での設定は終わりです。

# 実行環境構築

新しいライブラリはComposerで設定するようになっていました。
詳しい手順は[PHP Quickstart](https://developers.google.com/google-apps/calendar/quickstart/php?hl=ja)に書いてあります。
簡単にいうと

1. 今回の同期プログラム用のディレクトリ作成（例えばsync_calとします）
2. sync_calに移動してComposerをインストールします。```curl -sS https://getcomposer.org/installer | php```
3. sync_calの中にcomposer.pharというファイルができているはずです。
4. Quickstartの手順にあるように```php composer.phar require google/apiclient:^2.0```を実行します。
5. Quickstartにあるサンプルのquickstart.phpという名前で保存してください、ただしそのままだとうまく動かなかったので少しなおしました。（後述）
6. 前の手順で用意した【client_secret.json】も同じディレクトリに入れておいてください。

ディレクトリの構成はこのようになっているはずです。

```
sh-3.2$ tree -L 1 sync_cal
sync_cal
├── client_secret.json
├── composer.json
├── composer.lock
├── composer.phar
├── quickstart.php
└── vendor
```

環境構築がきちんとできていれば```php quickstart.php```できちんと動作するはずです。
１回目のアクセスではブラウザでの認可が必要になるため挙動が変わります。
メッセージに従いブラウザで所定のURLを開いて表示されたverification codeをコンソールで入力してください。
同じディレクトリにcalendar-php-quickstart.jsonというファイルができて２回目以降はサンプルが動くはずです。

## quickstart.phpの変更点

- 私は下記部分を２箇所変更しました。

```php
<?php
require_once __DIR__ . '/vendor/autoload.php';

date_default_timezone_set('Asia/Tokyo'); // ＊＊＊ 追加 ＊＊＊

define('APPLICATION_NAME', 'Google Calendar API PHP Quickstart');
define('CREDENTIALS_PATH',  __DIR__ . '/calendar-php-quickstart.json');// ＊＊＊ 変更 ＊＊＊
define('CLIENT_SECRET_PATH', __DIR__ . '/client_secret.json');

```


# プログラム構成

プログラム構成はこんな感じです。


```
sh-3.2$ tree -L 2 sync_cal
sync_cal
├── cal.php                      <-- プログラム
├── data.csv                     <-- データファイル
├── calendar-php-quickstart.json <-- quickstart.phpを実行したときにできる認証情報
├── client_secret.json
├── composer.json
├── composer.lock
├── composer.phar
├── quickstart.php
└── vendor
    ├── autoload.php
    ├── composer
    ├── firebase
    ├── google
    ├── guzzlehttp
    ├── monolog
    ├── phpseclib
    └── psr
```

data.csvはこんな感じにしてください。utf8でタブ区切りです。
開始時刻、終了時刻、場所、タイトル名称、説明、リンク（なければダミーで）になります。

```data.csv
2017-09-29T11:30:00+09:00	2017-09-29T12:30:00+09:00	場所1	名称1	説明1	http://example.com?no1
2017-09-29T12:30:00+09:00	2017-09-29T13:30:00+09:00	場所2	名称2	説明2	http://example.com?no2
2017-10-01T13:30:00+09:00	2017-10-01T14:30:00+09:00	場所3	名称3	説明3	http://example.com?no3
2017-11-10T14:30:00+09:00	2017-11-10T15:30:00+09:00	場所4	名称4	説明4	http://example.com?no4
```

本プログラムのcal.phpはこんな感じです。

+ data.csvを読み込み
+ Calendar API で同期用のカレンダーのイベントを読み込みます。
+ data.csvに無い情報は削除します。
+ 次に、data.csv新規・変更された情報を挿入します。

個人設定部分は適宜書きなおしてください。

```php:cal.php
<?php
require_once __DIR__ . '/vendor/autoload.php';

////////////////////////////////////////////////////////////
// 誤同期防止用にマイカレンダーの名称を明示する。
define('TARGET_CALENDAR', '<マイカレンダーの名称>');
// 同期用のデータファイル名
define('SYNC_DATA', 'data.csv');
////////////////////////////////////////////////////////////

// アプリケーション名
define('APPLICATION_NAME', 'Google Calendar API PHP');
// Google APIs > APIとサービス > 認証情報 > OAuth 2.0 クライアントID
// ダウンロード client_secret_xxxxをリネーム
define('CLIENT_SECRET_PATH', __DIR__ . '/client_secret.json');
// 認可情報を保存しておくファイル
define('CREDENTIALS_PATH', __DIR__ .'/calendar-sync-php.json');
// ※ 書き込みもするのでREADONLYではなく Google_Service_Calendar::CALENDAR にする！
define('SCOPES', implode(' ', array(Google_Service_Calendar::CALENDAR)));

// TZ日本指定
date_default_timezone_set('Asia/Tokyo');
// LC日本語指定
setlocale(LC_ALL,'ja_JP.UTF-8');

// データの記録用
$data = array();
// データファイル読み込み
$fp = fopen(SYNC_DATA,"r");
while ( $d = fgetcsv($fp,4096,"\t") ) {
	//0:開始時刻
	//1:終了時刻
	//2:場所
	//3:タイトル
	//4:説明文
	//5:URL
	$key = $d[0]."\t".$d[1]."\t".$d[2]."\t".$d[3]."\t".$d[4]."\t".$d[5];
	$data[md5($key)] = $key;
}

// イベント取得設定 オプション
$optParams = array(
    'orderBy' => 'startTime',
    'singleEvents' => TRUE
);

// Get the API client and construct the service object.
$client = getClient();
$service = new Google_Service_Calendar($client);

//カレンダー一覧
$cal_list = $service->calendarList->listCalendarList();

foreach ($cal_list['items'] as $calendar) {
    $calenderId = $calendar->id;
    $calender_name = $calendar->getSummary();
    echo($calender_name."\t".$calenderId."\n");
    
    // 指定のカレンダー以外はスキップ
    if ( $calender_name != TARGET_CALENDAR ) {
        continue;
    }
    
    $events = $service->events->listEvents($calenderId,$optParams);
    while(true) {
        
        foreach ($events->getItems() as $event) {

            $eventId = $event->getId();
            
            if ( $eventId == NULL ) {
                // var_dump($event);
                continue;
            }

            $source_title = "";
            if ( $event->source != NULL ) {
                $source_title = $event->source->title;
            }
            //カレンダーから読み込んだキーとデータファイルを比較してデータファイル側になければ削除
            if ( array_key_exists( $source_title , $data ) ) {
                //更新リストから削除
                // echo("Already exist:".$eventId."\t".$event->summary."\n");
                unset($data[$source_title]);
            } else {
                // echo("Delete:".$eventId."\t".$event->summary."\n");
                $service->events->delete( $calenderId , $eventId );
            }

        }

        $pageToken = $events->getNextPageToken();
        if ($pageToken) {
            $optParams = array('pageToken' => $pageToken);
            $events = $service->events->listEvents($calenderId, $optParams);
        } else {
            break;
        }

    } 

    //変更されたイベントの追加
    foreach ($data as $key => $value) {

        $d = explode("\t", $value);

        //終日イベントの判定
        $start_datetime = explode("T",$d[0]);
        $end_datetime   = explode("T",$d[1]);

        $all_day = FALSE;

        if ( $start_datetime[0] === $end_datetime[0] && $start_datetime[1] === "00:00:00+09:00" && $end_datetime[1] === "23:59:59+09:00" ) {
            $all_day = TRUE;
        }

        // イベント新規追加
        $event = new Google_Service_Calendar_Event();
        $event->setLocation( $d[2] );
        $event->setSummary( $d[3] );
        $event->setDescription( $d[4] );

        // 開始
        $start = new Google_Service_Calendar_EventDateTime();
        if ( $all_day ) {
            $start->setDate( $start_datetime[0] );
        } else {
            $start->setDateTime( $d[0] );
        }
        $event->setStart($start);

        // 終了
        $end = new Google_Service_Calendar_EventDateTime();
        if ( $all_day ) {
            $end->setDate( $end_datetime[0] );
        } else {
            $end->setDateTime( $d[1] );
        }
        $event->setEnd($end);

        // イベントのハッシュ値をsource->titleに設定
        $source = new Google_Service_Calendar_EventSource();
        $source->setTitle( $key );
        $source->setUrl( $d[5] );
        $event->setSource( $source );

        // イベント追加
        $service->events->insert( $calenderId,$event);

    }
}


exit;

/**
* Returns an authorized API client.
* @return Google_Client the authorized client object
*/
function getClient() {
    $client = new Google_Client();
    $client->setApplicationName(APPLICATION_NAME);
    $client->setScopes(SCOPES);
    $client->setAuthConfig(CLIENT_SECRET_PATH);
    $client->setAccessType('offline');
    
    // Load previously authorized credentials from a file.
    $credentialsPath = expandHomeDirectory(CREDENTIALS_PATH);
    if (file_exists($credentialsPath)) {
        $accessToken = json_decode(file_get_contents($credentialsPath), true);
    } else {
        // Request authorization from the user.
        $authUrl = $client->createAuthUrl();
        printf("Open the following link in your browser:\n%s\n", $authUrl);
        print 'Enter verification code: ';
        $authCode = trim(fgets(STDIN));
        
        // Exchange authorization code for an access token.
        $accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
        
        // Store the credentials to disk.
        if(!file_exists(dirname($credentialsPath))) {
            mkdir(dirname($credentialsPath), 0700, true);
        }
        file_put_contents($credentialsPath, json_encode($accessToken));
        printf("Credentials saved to %s\n", $credentialsPath);
    }
    $client->setAccessToken($accessToken);
    
    // Refresh the token if it's expired.
    if ($client->isAccessTokenExpired()) {
        $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
        file_put_contents($credentialsPath, json_encode($client->getAccessToken()));
    }
    return $client;
}

/**
* Expands the home directory alias '~' to the full path.
* @param string $path the path to expand.
* @return string the expanded path.
*/
function expandHomeDirectory($path) {
    $homeDirectory = getenv('HOME');
    if (empty($homeDirectory)) {
        $homeDirectory = getenv('HOMEDRIVE') . getenv('HOMEPATH');
    }
    return str_replace('~', realpath($homeDirectory), $path);
}

?>
```

# ぼやき

APIの説明書がCalender APIというよりGoogle Client APIのリファレンスとして共通化されているようでなんだかとってもわかりづらい...
Googleのサービスを横断的に使いたい人にはいいのかもしれませんが...