開発情報・ナレッジ

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

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

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

注意点

本番運用では、OAuthクライアントシークレット等をソースに直書きせず、
環境変数や安全な保管方法を検討してください。
APIのレート制限、トークン期限、ネットワーク制限(外部通信可否)にご注意ください。
Google Meet の同時発行を行う場合は、Google Calendar APIの
conferenceData
を使用します。

実装の概要

今回のプログラムは、SPIRALのフォームのサンキューページのPHPから起動され、以下の流れで動作します。

事前準備. 予約DB作成、必要フィールド追加、Google側の認証情報の取得、サンクスメール配信設定
1.
$SPIRAL->getContextByFieldTitle("recordId")
/
$SPIRAL->getParam()
で値を取得
2. Google OAuth(refresh_token方式)でアクセストークンを取得
3. Google Calendar APIでイベントを作成し、
event_id
/
event_url
(必要なら
meeting_url
)を取得
4. SPIRALのDBへイベント情報を書き戻し(
doUpdate()

5. 更新完了後に
$apiCommunicator->request('deliver_thanks', 'send', $request);
を呼び出してサンクスメールを送信

事前準備

予約DB(イベント情報を書き戻すDB)に、最低限以下のフィールド(識別名)を用意してください。
サンクスメールについては別途メール送信だけを行うダミーフォームを作成して、
サンクスメール配信設定を行いそのメール配信IDを控えてください。

用途 識別名 型(例)
レコードID recordId 数字・記号・アルファベット(32byte) ※主キー
予約者名 name テキスト
メール mail メールアドレス
開始日時 start_datetime 日付(○年○月○日○時○分○秒)
終了日時 end_datetime 日付(○年○月○日○時○分○秒)
イベントID(書き戻し) event_id テキスト
イベントURL(書き戻し) event_url テキスト
Meet URL(書き戻し) meeting_url テキスト

設定方法

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

認証情報(Google)について

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


フォーム>サンキューページ>ソース編集
<?php
    // ------------------------------------------------------------
    // 読み取りフィールド(フォーム側の識別名):
    // - recordId 自動初番 主キー(getContextByFieldTitle("recordId") で取得)
    // - name
    // - mail
    // - start_datetime (例: 2025年12月30日1時0分0秒)
    // - end_datetime   (例: 2025年12月30日2時0分0秒)
    //
    // 更新フィールド(DB側の識別名):
    // - event_id
    // - event_url
    // - meeting_url(Google Meet を同時発行する場合)
    // ------------------------------------------------------------

    // --- SPIRAL(レコードを書き戻すためのAPI設定) ---
    define('API_TOKEN_TITLE', '');
    define('DB_TITLE', '');
    define('ID_FIELD_TITLE', 'recordId');
    define('THANKS_RULE_ID', '');

    //原則変更不要
    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 updateSpiralRecordById(string $recordId, array $updateData): int
    {
        global $SPIRAL;
        if (!isset($SPIRAL)) {
            throw new Exception('SPIRAL runtime is not available.');
        }
        if (DB_TITLE === '') {
            throw new Exception('DB_TITLE is empty.');
        }
        if (ID_FIELD_TITLE === '') {
            throw new Exception('ID_FIELD_TITLE is empty.');
        }

        if (API_TOKEN_TITLE !== '') {
            $SPIRAL->setApiTokenTitle(API_TOKEN_TITLE);
        }

        $db = $SPIRAL->getDataBase(DB_TITLE);
        $db->addEqualCondition(ID_FIELD_TITLE, $recordId);
        $count = $db->doUpdate($updateData);
        return (int)$count;
    }

    function sendThanksMail(string $ruleId, string $recordId): void
    {
        global $SPIRAL;
        if (!isset($SPIRAL)) {
            throw new Exception('SPIRAL runtime is not available.');
        }

        $apiCommunicator = $SPIRAL->getSpiralApiCommunicator();
        $request = new SpiralApiRequest();
        $request->put('rule_id', $ruleId);
        $request->put('id', $recordId);

        $response = $apiCommunicator->request('deliver_thanks', 'send', $request);
        if ((int)$response->get('code') !== 0) {
            throw new Exception('deliver_thanks/send error (code:' . $response->get('code') . ')');
        }
    }

    function parseRecordDateTime(string $value, string $timezone = DEFAULT_TIMEZONE): DateTimeImmutable
    {
        $trimmed = trim($value);

        $jpPattern = '/^(\d{4})年(\d{1,2})月(\d{1,2})日(\d{1,2})時(\d{1,2})分(\d{1,2})秒$/u';
        if (preg_match($jpPattern, $trimmed, $m) === 1) {
            $normalized = sprintf(
                '%04d-%02d-%02d %02d:%02d:%02d',
                (int)$m[1],
                (int)$m[2],
                (int)$m[3],
                (int)$m[4],
                (int)$m[5],
                (int)$m[6]
            );
            $dt = new DateTimeImmutable($normalized, new DateTimeZone($timezone));
            return $dt->setTimezone(new DateTimeZone($timezone));
        }

        $dt = new DateTimeImmutable($trimmed);
        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.');
        }

        $recordId = (string)$SPIRAL->getContextByFieldTitle('recordId');
        if ($recordId === '') {
            throw new Exception('Record ID is missing.');
        }

        $username = (string)$SPIRAL->getParam('name');
        $mail = (string)$SPIRAL->getParam('mail');
        $startRaw = (string)$SPIRAL->getParam('start_datetime');
        $endRaw = (string)$SPIRAL->getParam('end_datetime');

        $eventIdCurrent = (string)$SPIRAL->getParam('event_id');
        $eventUrlCurrent = (string)$SPIRAL->getParam('event_url');

        if ($eventIdCurrent !== '' || $eventUrlCurrent !== '') {
            return;
        }

        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 = [
            'event_id' => $eventId,
            'event_url' => $eventUrl,
        ];
        if ($meetingUrl !== '') {
            $updateData['meeting_url'] = $meetingUrl;
        }

        updateSpiralRecordById($recordId, $updateData);

        $ruleId = (string)THANKS_RULE_ID;
        if ($ruleId !== '') {
            sendThanksMail($ruleId, $recordId);
        }

    } catch (Exception $e) {
        try {
            if (isset($SPIRAL)) {
                $recordId = (string)$SPIRAL->getContextByFieldTitle('recordId');
                $eventIdCurrent = (string)$SPIRAL->getParam('event_id');
                $eventUrlCurrent = (string)$SPIRAL->getParam('event_url');

                if ($recordId !== '' && $eventIdCurrent === '' && $eventUrlCurrent === '') {
                    updateSpiralRecordById($recordId, [
                        'event_id' => '',
                        'event_url' => '',
                        'meeting_url' => '',
                    ]);
                }
            }
        } catch (Exception $ignored) {
        }

        throw $e;
    }

                
例:設定値(抜粋)
    // --- SPIRAL(レコードを書き戻すためのAPI設定) ---
    define('API_TOKEN_TITLE', '');
    define('DB_TITLE', '');
    define('ID_FIELD_TITLE', 'recordId');
    define('THANKS_RULE_ID', '');

    //原則変更不要
    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
が入っている場合は、誤って消さないように空更新をスキップします。

実行結果

サンキューページで本プログラムが実行されると、対象レコードに以下が反映されます。

event_id
にGoogleカレンダーのイベントID
event_url
にGoogleカレンダーのイベントURL(htmlLink)
meeting_url
(ENABLE_MEET=trueの場合に設定される可能性があります)
更新完了後に
deliver_thanks/send
が実行されます(メール送信)

まとめ

本記事では、SPIRALのサンキューページを起点に Googleカレンダーの予定を自動作成し、
SPIRALのDBへ書き戻すサンプルを紹介しました。
不具合がある場合は、エラーメッセージ(HTTPコードとレスポンス)を確認し、
OAuth設定・スコープ・権限・カレンダー共有設定などを見直してください。
解決しない場合はこちら コンテンツに関しての
要望はこちら