開発情報・ナレッジ

投稿者: ShiningStar株式会社 2026年1月13日 (火)

DBにレコードが登録された際にGoogleカレンダーに反映するサンプルプログラム

SPIRALのDBトリガーを利用して、予約レコードの登録・更新をきっかけに
Googleカレンダーへ予定(イベント)を自動作成し、
作成したイベント情報(イベントID/URL、必要ならGoogle Meet URL)を同じレコードへ書き戻す
サンプルプログラムの実装方法を紹介します。
予定作成とURL転記を自動化できるため、運用側の手作業を削減できます。
また、レコード更新が完了したタイミングで アクション実行API を呼び出し、
後続処理(メール送信など)へつなげる構成です。

注意点

本番運用では、APIキーやOAuthクライアントシークレット等をソースに直書きせず、
環境変数や安全な保管方法を検討してください。
APIのレート制限、トークン期限、ネットワーク制限(外部通信可否)にご注意ください。
DBトリガーは同時実行の可能性があるため、同一レコードの二重発行を避ける設計が必要です。
(本コードでは
event_id
/
event_url
が既に入っていれば処理を終了します)。
Google Meet の同時発行を行う場合は、Google Calendar APIの
conferenceData
を使用します。

実装の概要

今回のプログラムは、SPIRALのDBトリガーから起動され、以下の流れで動作します。

事前準備. 予約DB作成、必要フィールド追加、DBトリガー設定、Google側の認証情報の取得
1. DBトリガーで
$SPIRAL->getRecord()
から更新対象レコードを取得
2. Google OAuth(refresh_token方式)でアクセストークンを取得
3. Google Calendar APIでイベントを作成し、
event_id
/
event_url
(必要なら
meeting_url
)を取得
4. 取得した値を SPIRAL API(PATCH)で同一レコードへ書き戻し
5. 更新完了後に
/apps/{appId}/actions/{actionId}/run
を呼び出してアクションを実行

事前準備

予約DB(イベント情報を書き戻すDB)に、最低限以下のフィールド(識別名)を用意してください。

用途 識別名 型(例)
予約者名 name テキスト
メール mail メールアドレス
開始日時 start_datetime 日時
終了日時 end_datetime 日時
イベントID(書き戻し) event_id テキスト
イベントURL(書き戻し) event_url テキスト
Meet URL(書き戻し) meeting_url テキスト

設定方法

下記PHPの先頭にある定数を、環境に合わせて設定します。

認証情報(Google)について

本プログラムの動作には、Google Calendar の 認証情報の取得・設定 が必要です。
取得画面や手順は下記リンクを参考にご確認ください。


DB>トリガ>登録トリガ>非同期アクション>PHP実行
<?php
    // ------------------------------------------------------------
    // 読み取りフィールド(DB側の識別名):
    // - name
    // - mail
    // - start_datetime (例: 2025-12-29T16:00:00Z)
    // - end_datetime   (例: 2025-12-29T17:00:00Z)
    //
    // 更新フィールド(DB側の識別名):
    // - event_id
    // - event_url
    // - meeting_url(Google Meet を同時発行する場合)
    // ------------------------------------------------------------

    // --- SPIRAL(レコードを書き戻すためのAPI設定) ---
    define('API_URL', 'https://api.spiral-platform.com/v1');
    define('API_KEY', '');
    define('APP_ROLE', '');
    define('APP_ID', '');
    define('DB_ID', '');
    define('ACTION_ID', '');

    //原則変更不要
    define('SPIRAL_API_VERSION', '1.1');
    define('DEFAULT_TIMEZONE', 'Asia/Tokyo');

    // --- Google Calendar(OAuth refresh_token方式) ---
    define('GOOGLE_OAUTH_CLIENT_ID', '');
    define('GOOGLE_OAUTH_CLIENT_SECRET', '');
    // --- Refresh Token(スコープ例: `https://www.googleapis.com/auth/calendar.events`)
    define('GOOGLE_OAUTH_REFRESH_TOKEN', '');

    // --- Google Calendar(イベント作成先) ---
    define('GOOGLE_CALENDAR_ID', 'primary');
    define('ENABLE_MEET', true);

    function apiRequest(string $method, string $path, ?array $data = null): array
    {
        $headers = [
            'Authorization:Bearer ' . API_KEY,
            'X-Spiral-Api-Version: ' . SPIRAL_API_VERSION,
            'Content-Type:application/json',
        ];
        if (APP_ROLE !== '') {
            $headers[] = 'X-Spiral-App-Role: ' . APP_ROLE;
        }

        $url = rtrim(API_URL, '/') . $path;

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);

        if ($data !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE));
        }

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlErr = curl_error($ch);
        curl_close($ch);

        if ($response === false) {
            throw new Exception('cURL Error: ' . $curlErr);
        }

        $decoded = json_decode($response, true);
        if ($httpCode < 200 || $httpCode >= 300) {
            throw new Exception('SPIRAL API Error (HTTP ' . $httpCode . '): ' . $response);
        }

        return is_array($decoded) ? $decoded : [];
    }

    function updateSpiralRecord(string $recordId, array $updateData): array
    {
        $path = '/apps/' . APP_ID . '/dbs/' . DB_ID . '/records/' . $recordId;
        return apiRequest('PATCH', $path, $updateData);
    }

    function runSpiralAction(string $actionId, string $recordId): array
    {
        $path = '/apps/' . APP_ID . '/actions/' . $actionId . '/run';
        return apiRequest('POST', $path, [
            'recordId' => (string)$recordId,
        ]);
    }

    function parseRecordDateTime(string $value, string $timezone = DEFAULT_TIMEZONE): DateTimeImmutable
    {
        $dt = new DateTimeImmutable($value);
        return $dt->setTimezone(new DateTimeZone($timezone));
    }

    class GoogleCalendarClient
    {
        private array $config;
        private ?string $accessToken = null;

        public function __construct(array $config)
        {
            $this->config = $config;
        }

        public function createEvent(string $calendarId, string $summary, DateTimeImmutable $startTime, DateTimeImmutable $endTime, string $timezone, string $attendeeEmail = ''): array
        {
            $token = $this->getAccessToken();

            $query = '';
            if (ENABLE_MEET) {
                $query = '?conferenceDataVersion=1';
            }

            $url = 'https://www.googleapis.com/calendar/v3/calendars/' . rawurlencode($calendarId) . '/events' . $query;

            $payload = [
                'summary' => $summary,
                'start' => [
                    'dateTime' => $startTime->format(DateTimeInterface::RFC3339),
                    'timeZone' => $timezone,
                ],
                'end' => [
                    'dateTime' => $endTime->format(DateTimeInterface::RFC3339),
                    'timeZone' => $timezone,
                ],
            ];

            if ($attendeeEmail !== '') {
                $payload['attendees'] = [
                    ['email' => $attendeeEmail],
                ];
            }

            if (ENABLE_MEET) {
                $requestId = str_replace('.', '', uniqid('req_', true));
                $payload['conferenceData'] = [
                    'createRequest' => [
                        'requestId' => $requestId,
                        'conferenceSolutionKey' => [
                            'type' => 'hangoutsMeet',
                        ],
                    ],
                ];
            }

            $ch = curl_init($url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE));
            curl_setopt($ch, CURLOPT_HTTPHEADER, [
                'Authorization: Bearer ' . $token,
                'Content-Type: application/json',
            ]);

            $response = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $curlErr = curl_error($ch);
            curl_close($ch);

            if ($response === false) {
                throw new Exception('Google Calendar API cURL Error: ' . $curlErr);
            }
            if ($httpCode >= 400) {
                throw new Exception('Google Calendar API Error (HTTP ' . $httpCode . '): ' . $response);
            }

            $data = json_decode($response, true);
            if (!is_array($data)) {
                throw new Exception('Google Calendar API Error: invalid JSON response: ' . $response);
            }

            $meetUrl = (string)($data['hangoutLink'] ?? '');
            if ($meetUrl === '' && isset($data['conferenceData']['entryPoints']) && is_array($data['conferenceData']['entryPoints'])) {
                foreach ($data['conferenceData']['entryPoints'] as $ep) {
                    if (is_array($ep) && ($ep['entryPointType'] ?? '') === 'video' && !empty($ep['uri'])) {
                        $meetUrl = (string)$ep['uri'];
                        break;
                    }
                }
            }

            return [
                'event_id' => (string)($data['id'] ?? ''),
                'event_url' => (string)($data['htmlLink'] ?? ''),
                'meeting_url' => $meetUrl,
            ];
        }

        private function getAccessToken(): string
        {
            if ($this->accessToken) {
                return $this->accessToken;
            }

            if (!empty($this->config['oauth_refresh_token'])) {
                $ch = curl_init('https://oauth2.googleapis.com/token');
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_POST, true);
                curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
                    'client_id' => $this->config['oauth_client_id'] ?? '',
                    'client_secret' => $this->config['oauth_client_secret'] ?? '',
                    'refresh_token' => $this->config['oauth_refresh_token'] ?? '',
                    'grant_type' => 'refresh_token',
                ]));

                $response = curl_exec($ch);
                $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                $curlErr = curl_error($ch);
                $data = json_decode((string)$response, true);
                curl_close($ch);

                if ($response === false) {
                    throw new Exception('Google Auth cURL Error: ' . $curlErr);
                }
                if ($httpCode < 200 || $httpCode >= 300) {
                    throw new Exception('Google Auth Error (HTTP ' . $httpCode . '): ' . $response);
                }
                if (!is_array($data)) {
                    throw new Exception('Google Auth Error: invalid JSON response: ' . $response);
                }
                if (isset($data['error'])) {
                    $desc = $data['error_description'] ?? $data['error'];
                    throw new Exception('Google Auth Error: ' . $desc);
                }

                $this->accessToken = (string)($data['access_token'] ?? '');
                if ($this->accessToken === '') {
                    throw new Exception('Google Auth Error: access_token is empty: ' . $response);
                }
                return $this->accessToken;
            }

            throw new Exception('Google auth config is missing. Set oauth_client_id/oauth_client_secret/oauth_refresh_token.');
        }
    }

    $googleConfig = [
        'oauth_client_id' => GOOGLE_OAUTH_CLIENT_ID,
        'oauth_client_secret' => GOOGLE_OAUTH_CLIENT_SECRET,
        'oauth_refresh_token' => GOOGLE_OAUTH_REFRESH_TOKEN,
    ];

    try {
        if (!isset($SPIRAL)) {
            throw new Exception('SPIRAL runtime is not available.');
        }

        $record = $SPIRAL->getRecord();
        $item = $record['item'] ?? null;

        if (!is_array($item)) {
            throw new Exception('Record is empty.');
        }

        $recordId = (string)($item['_id'] ?? '');
        $revision = (string)($item['_revision'] ?? '');
        if ($recordId === '') {
            throw new Exception('Record ID is missing.');
        }
        if ($revision === '') {
            throw new Exception('Record revision is missing.');
        }

        if (!empty($item['event_id']) || !empty($item['event_url'])) {
            return;
        }

        $username = (string)($item['name'] ?? '');
        $mail = (string)($item['mail'] ?? '');
        $startRaw = (string)($item['start_datetime'] ?? '');
        $endRaw = (string)($item['end_datetime'] ?? '');

        if ($startRaw === '' || $endRaw === '') {
            throw new Exception('Required fields are missing.');
        }

        $startAt = parseRecordDateTime($startRaw);
        $endAt = parseRecordDateTime($endRaw);
        if ($endAt <= $startAt) {
            throw new Exception('Invalid start/end datetime.');
        }

        $summary = '予約: ' . ($username !== '' ? $username . '様' : '');

        $client = new GoogleCalendarClient($googleConfig);
        $event = $client->createEvent(GOOGLE_CALENDAR_ID, $summary, $startAt, $endAt, DEFAULT_TIMEZONE, $mail);

        $eventId = (string)($event['event_id'] ?? '');
        $eventUrl = (string)($event['event_url'] ?? '');
        $meetingUrl = (string)($event['meeting_url'] ?? '');

        if ($eventId === '' || $eventUrl === '') {
            throw new Exception('Google Calendar event creation failed.');
        }

        $updateData = [
            '_revision' => $revision,
            'event_id' => $eventId,
            'event_url' => $eventUrl,
        ];
        if ($meetingUrl !== '') {
            $updateData['meeting_url'] = $meetingUrl;
        }

        updateSpiralRecord($recordId, $updateData);

        $actionId = (string)ACTION_ID;
        if ($actionId !== '') {
            runSpiralAction($actionId, $recordId);
        }

    } catch (Exception $e) {
        try {
            if (isset($SPIRAL)) {
                $record = $SPIRAL->getRecord();
                $item = $record['item'] ?? null;
                $recordId = is_array($item) ? (string)($item['_id'] ?? '') : '';
                $revision = is_array($item) ? (string)($item['_revision'] ?? '') : '';

                $hasEventId = is_array($item) && !empty($item['event_id']);
                $hasEventUrl = is_array($item) && !empty($item['event_url']);

                if ($recordId !== '' && $revision !== '' && APP_ID !== '' && DB_ID !== '' && !$hasEventId && !$hasEventUrl) {
                    updateSpiralRecord($recordId, [
                        '_revision' => $revision,
                        'event_id' => '',
                        'event_url' => '',
                        'meeting_url' => '',
                    ]);
                }
            }
        } catch (Exception $ignored) {
        }

        throw $e;
    }

                
例:設定値(抜粋)
    // --- SPIRAL(レコードを書き戻すためのAPI設定) ---
    define('API_URL', 'https://api.spiral-platform.com/v1');
    define('API_KEY', '');
    define('APP_ROLE', '');
    define('APP_ID', '');
    define('DB_ID', '');
    define('ACTION_ID', '');

    //原則変更不要
    define('SPIRAL_API_VERSION', '1.1');
    define('DEFAULT_TIMEZONE', 'Asia/Tokyo');

    // --- Google Calendar(OAuth refresh_token方式) ---
    define('GOOGLE_OAUTH_CLIENT_ID', '');
    define('GOOGLE_OAUTH_CLIENT_SECRET', '');
    // --- Refresh Token(スコープ例: `https://www.googleapis.com/auth/calendar.events`)
    define('GOOGLE_OAUTH_REFRESH_TOKEN', '');

    // --- Google Calendar(イベント作成先) ---
    define('GOOGLE_CALENDAR_ID', 'primary');
    define('ENABLE_MEET', true);
                

エラーハンドリング(失敗時の動き)

イベント作成やAPI呼び出しで例外が発生した場合は、catchで「未発行」の状態に戻すために

event_id
/
event_url
(必要なら
meeting_url
)を空に書き戻します。
ただし、既に
event_id
/
event_url
が入っている場合は、誤って消さないように空更新をスキップします。

実行結果

DBトリガーで本プログラムが実行されると、対象レコードに以下が反映されます。

event_id
にGoogleカレンダーのイベントID
event_url
にGoogleカレンダーのイベントURL(htmlLink)
meeting_url
(ENABLE_MEET=trueの場合に設定される可能性があります)
更新完了後に
ACTION_ID
のアクションが実行されます(メール送信や通知などの後続処理)

まとめ

本記事では、SPIRALのDBトリガーを起点に Googleカレンダーの予定を自動作成し、
SPIRALのDBへ書き戻すサンプルを紹介しました。
さらに更新完了後にアクション実行APIを呼び出すことで、
通知やワークフローなどの後続処理まで一気通貫で自動化できます。
不具合がある場合は、エラーメッセージ(HTTPコードとレスポンス)を確認し、
APIキー・OAuth設定・スコープ・権限・カレンダー共有設定などを見直してください。
解決しない場合はこちら コンテンツに関しての
要望はこちら