明けましておめでとうございます!永井です!
本年も「カルテットコミュニケーションズ開発部ブログ」を、宜しくお願い致します。

はじめに

弊社では会社の公休日というのをGoogleカレンダーを利用して全社員に公開されています。
2019年1月現在、2020年末ごろまで登録されていますが、年初の時点で今年中の予定が公開されているというのは、一社員としてはとても助かりますよね。
折角、Googleカレンダーというアクセスしやすい形で公開されているので、プログラムで「特定の日が休みかどうか」を判定できると、いろいろ便利かなと思い、実際に検証してみました。

検証

前提

弊社は完全週休2日制で、祝日も休みとなりますので、

  • 土日
  • 祝日
  • 会社が定める公休日

という条件で、その日が休みかどうかを判定することができそうです。
ちなみに会社の公休日は主に年末年始とお盆休みが定められており、年間休日120日以上は確保されています。詳細はこちらをごらんください → 募集要項

土日の判定

これは結構簡単に判定できますよね。
例えばこんな感じ。

<?php

function isWeekend(\DateTimeInterface $target): boolean
{
    return in_array($target->format('w'), ['0', '6'], true);
}

祝日と会社の公休日の判定

祝日の判定は、77web/Japanese.Holidayという素敵なライブラリがあるのですが、今回はGoogleカレンダーをつかうことになりますので、祝日も日本の祝日というGoogleカレンダーで判定することにします。
Googleカレンダーの情報を取得するには、APIを利用したいと思いますが、G Suite配下のプライベートなカレンダーへアクセスする場合には、 サービスアカウントを利用する方がお手軽なので、今回はこちらを利用して進めていきます。

GCPのプロジェクトを作成する

GCPのWebコンソールへアクセスして、プロジェクトを作成してください。

gcp1

GCPのWEBコンソールで、Google Calendar APIを有効にする

API とサービス ページから、Google Calendar API を有効にしてください。

gcp2

サービスアカウントの作成

  • 役割は特に指定しなくてもOK
  • キーのタイプは特にこだわりがなければJSON

というで進めていきます。

service account

会社の公休日カレンダーのサービスアカウントへの共有とIDの確認

カレンダーの詳細ページにいくと、特定のユーザーとの共有で先程追加したサービスアカウントが選べるようになっていますので、閲覧権限(すべての予定の詳細)で共有します。
ちなみに 予定の時間枠のみ表示(詳細は非表示) でも大丈夫かなと試してみましたが、権限が不足しているみたいでした。

また、カレンダーIDも確認してください。

calendar id

日本の祝日カレンダーのIDの確認

日本の祝日 は既に全ユーザーに共有されていますので、何もしなくても利用できます。
IDは ja.japanese#holiday@group.v.calendar.google.com になります。

利用するライブラリ

Googleが用意してくれているgoogleapis/google-api-php-clientを利用します。
composerで簡単にインストールできます。

$ composer require google/apiclient:"^2.0"

特定の日付に予定があるか判定するコード

以下のような関数を用意してみました。

<?php

function hasItems($calendarId, \DateTimeInterface $date)
{
    $credentialsPath = 'service-account-key.json';
    putenv('GOOGLE_APPLICATION_CREDENTIALS='.$credentialsPath);

    $client = new Google_Client();
    $client->useApplicationDefaultCredentials();

    $client->addScope(Google_Service_Calendar::CALENDAR_EVENTS_READONLY);
    $service = new Google_Service_Calendar($client);

    $optParams = [
        'orderBy' => 'startTime',
        'singleEvents' => TRUE,
        'timeMin' => (new \DateTime($date->format('Y-m-d 00:00:00')))->format(DATE_RFC3339),
        'timeMax' => (new \DateTime($date->format('Y-m-d 23:59:59')))->format(DATE_RFC3339),
        'timeZone' => 'Asia/Tokyo',
    ];
    $results = $service->events->listEvents($calendarId, $optParams);

    return \count($results->getItems()) > 0;
}

見ていただければわかると思いますが、

特定の日付に1件以上予定があるかどうか

というロジックになります。

気をつける点はタイムゾーンの設定です。
PHPのタイムゾーン、カレンダーのタイムゾーンなど色々ありますので、それぞれ適切に設定する必要があります。

また、Googleクライアントにはスコープを指定する必要があります。
Google_Service_Calendar::CALENDAR_EVENTS_READONLY または Google_Service_Calendar::CALENDAR_READONLY を指定する事でカレンダーおよびそのイベントにアクセスする事ができます。

最終的なコード

<?php

date_default_timezone_set('Asia/Tokyo');
require_once 'vendor/autoload.php';

$japaneseHolidayCalendarId = 'ja.japanese#holiday@group.v.calendar.google.com';
$quartetDayOffCalendarId = '会社の公休日のカレンダーID';

// 2019年の1月1日から31日を判定して出力する
foreach (range(1, 31) as $day) {
    $target = (new \DateTime())->setDate(2019, 01, $day)->setTime(0,0,0);
    if (
        isWeekend($target) ||
        hasItems($japaneseHolidayCalendarId, $target) ||
        hasItems($quartetDayOffCalendarId, $target)
    ) {
        printf('%s is day off.'.PHP_EOL, $target->format('Y-m-d'));
    } else {
        printf('%s is work day.'.PHP_EOL, $target->format('Y-m-d'));
    }
}

function isWeekend(\DateTimeInterface $date)
{
    return in_array($date->format('w'), [0,6], false);
}

function hasItems($calendarId, \DateTimeInterface $date)
{
    $credentialsPath = 'service-account-key.json';
    putenv('GOOGLE_APPLICATION_CREDENTIALS='.$credentialsPath);

    $client = new Google_Client();
    $client->useApplicationDefaultCredentials();

    $client->addScope(Google_Service_Calendar::CALENDAR_EVENTS_READONLY);
    $service = new Google_Service_Calendar($client);

    $optParams = [
        'orderBy' => 'startTime',
        'singleEvents' => TRUE,
        'timeMin' => (new \DateTime($date->format('Y-m-d 00:00:00')))->format(DATE_RFC3339),
        'timeMax' => (new \DateTime($date->format('Y-m-d 23:59:59')))->format(DATE_RFC3339),
        'timeZone' => 'Asia/Tokyo',
    ];
    $results = $service->events->listEvents($calendarId, $optParams);

    return \count($results->getItems()) > 0;
}

実行した結果は以下の通り。

2019-01-01 is day off.
2019-01-02 is day off.
2019-01-03 is day off.
2019-01-04 is work day.
2019-01-05 is day off.
2019-01-06 is day off.
2019-01-07 is work day.
2019-01-08 is work day.
2019-01-09 is work day.
2019-01-10 is work day.
2019-01-11 is work day.
2019-01-12 is day off.
2019-01-13 is day off.
2019-01-14 is day off.
2019-01-15 is work day.
2019-01-16 is work day.
2019-01-17 is work day.
2019-01-18 is work day.
2019-01-19 is day off.
2019-01-20 is day off.
2019-01-21 is work day.
2019-01-22 is work day.
2019-01-23 is work day.
2019-01-24 is work day.
2019-01-25 is work day.
2019-01-26 is day off.
2019-01-27 is day off.
2019-01-28 is work day.
2019-01-29 is work day.
2019-01-30 is work day.
2019-01-31 is work day.

意図したとおり実行されました!

さいごに

いかがでしたでしょうか。
簡単ではありますが、いろんなところで活躍する場面がありそうですよね。
とても小さい機能ですが、色んな所で使い所がありそうなので、ライブラリ化して社内で公開できると良いかなと思っています!