SPIRALのPHPを利用して、予約フォームの登録をきっかけに
Googleカレンダーへ予定(イベント)を自動作成し、
作成したイベント情報(イベントID/URL、必要ならGoogle Meet URL)を同じレコードへ書き戻す
サンプルプログラムの実装方法を紹介します。
予定作成とURL転記を自動化できるため、運用側の手作業を削減できます。
また、レコード更新が完了したタイミングで サンクスメール配信API を呼び出し、
メール配信処理へつなげる構成です。
注意点
環境変数や安全な保管方法を検討してください。
・ APIのレート制限、トークン期限、ネットワーク制限(外部通信可否)にご注意ください。
・ Google Meet の同時発行を行う場合は、Google Calendar APIの
conferenceDataを使用します。
実装の概要
今回のプログラムは、SPIRALのフォームのサンキューページのPHPから起動され、以下の流れで動作します。
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 | テキスト |
| メール | メールアドレス | |
| 開始日時 | start_datetime | 日付(○年○月○日○時○分○秒) |
| 終了日時 | end_datetime | 日付(○年○月○日○時○分○秒) |
| イベントID(書き戻し) | event_id | テキスト |
| イベントURL(書き戻し) | event_url | テキスト |
| Meet URL(書き戻し) | meeting_url | テキスト |
設定方法
下記PHPの先頭にある定数を、環境に合わせて設定します。
認証情報(Google)について
本プログラムの動作には、Google Calendar の 認証情報の取得・設定 が必要です。
取得画面や手順は下記リンクを参考にご確認ください。
https://developers.google.com/calendar/api/guides/overview
・ OAuth 2.0(Google)
https://developers.google.com/identity/protocols/oauth2
フォーム>サンキューページ>ソース編集
<?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のDBへ書き戻すサンプルを紹介しました。
不具合がある場合は、エラーメッセージ(HTTPコードとレスポンス)を確認し、
OAuth設定・スコープ・権限・カレンダー共有設定などを見直してください。