SPIRALのDBトリガーを利用して、予約レコードの登録・更新をきっかけに
Googleカレンダーへ予定(イベント)を自動作成し、
作成したイベント情報(イベントID/URL、必要ならGoogle Meet URL)を同じレコードへ書き戻す
サンプルプログラムの実装方法を紹介します。
予定作成とURL転記を自動化できるため、運用側の手作業を削減できます。
また、レコード更新が完了したタイミングで アクション実行API を呼び出し、
後続処理(メール送信など)へつなげる構成です。
注意点
環境変数や安全な保管方法を検討してください。
・ APIのレート制限、トークン期限、ネットワーク制限(外部通信可否)にご注意ください。
・ DBトリガーは同時実行の可能性があるため、同一レコードの二重発行を避ける設計が必要です。
(本コードでは
event_id/
event_urlが既に入っていれば処理を終了します)。
・ Google Meet の同時発行を行う場合は、Google Calendar APIの
conferenceDataを使用します。
実装の概要
今回のプログラムは、SPIRALのDBトリガーから起動され、以下の流れで動作します。
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 | テキスト |
| メール | メールアドレス | |
| 開始日時 | 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
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へ書き戻すサンプルを紹介しました。
さらに更新完了後にアクション実行APIを呼び出すことで、
通知やワークフローなどの後続処理まで一気通貫で自動化できます。
不具合がある場合は、エラーメッセージ(HTTPコードとレスポンス)を確認し、
APIキー・OAuth設定・スコープ・権限・カレンダー共有設定などを見直してください。