PHP だけで Gemini API を Q&A 形式で呼び出す実装メモ
n8n などの外部 Workflow ツールに頼らず、PHP + ファイルキャッシュ だけで
Gemini API を Q&A 形式で呼び出す仕組みを作ったので紹介します。

概要
Markdown 記事の中に[BEMANIシリーズについて教えて]@(gemini-answer)と書くだけで、Gemini が回答を生成して表示してくれる仕組みです。
@ BEMANIシリーズについて教えてBEMANI(ビーマニ)シリーズは、コナミグループが展開する音楽ゲームの総称です。1997年に登場した「beatmania(ビートマニア)」というゲームが大ヒットしたことから、その名にちなんでシリーズ名が付けられました。
主な代表作には、足元のパネルを踏んで踊る「DanceDanceRevolution」、9つのボタンを叩く「pop'n music」、画面のパネルをタッチする「jubeat」、つまみを回して操作する「SOUND VOLTEX」などがあります。
音楽に合わせてボタンを操作する「音ゲー」というジャンルを確立した先駆け的な存在であり、現在もゲームセンターを中心に多くのファンに愛されています。こちらの要約は Google by Gemini によって作られました
仕組みのポイントは ファイルキャッシュ です。
一度生成した回答は .json ファイルに保存し、
2回目以降はそのファイルを読むだけで API を呼び出しません。
フローのおさらい
記事表示リクエスト
└─ キャッシュ .json が存在しない?
├─ YES → Gemini API を呼び出して .json に保存
└─ NO → .json を読んで表示(API 呼び出し不要)
実装コード
1. Gemini API 呼び出しクラス
/**
* Gemini API を使った Q&A 回答生成
*
* 呼び出し例: * GeminiAnswer::generate('/path/to/cache/my-question.json', [
* 'message' => '質問文をここに入れる',
* 'file' => 'my-question',
* ]);
*/
class GeminiAnswer
{
private const API_KEY = 'YOUR_API_KEY';
private const API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/'
. 'gemini-3.0-flash:generateContent?key=';
/**
* 質問を Gemini API に投げて回答を JSON ファイルに保存する
*
* @param string $cache_path 保存先の .json ファイルの絶対パス
* @param array $data ['message' => '質問文', 'file' => 'キャッシュID']
*/
public static function generate(string $cache_path, array $data): void
{
$question = trim($data['message'] ?? '');
if ($question === '') {
self::saveError($cache_path, '質問が空です');
return;
}
// ─── プロンプト ───────────────────────────────────────────
$prompt = <<あなたは親切な AI アシスタントです。以下の質問に日本語で分かりやすく回答してください。
回答は簡潔にまとめ、HTML タグやマークダウン記法は使わないでください。
質問:{$question}
PROMPT;
// ─── リクエストデータ ──────────────────────────────────
$body = json_encode([
'contents' => [[
'parts' => [['text' => $prompt]]
]],
'generationConfig' => [
'temperature' => 0.7,
'topK' => 40,
'topP' => 0.95,
'maxOutputTokens' => 2000,
],
]);
// ─── cURL で API 呼び出し ──────────────────────────────
$ch = curl_init(self::API_URL . self::API_KEY);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_CONNECTTIMEOUT_MS => 5000,
CURLOPT_TIMEOUT_MS => 30000,
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$errno = curl_errno($ch);
curl_close($ch);
// ─── エラーチェック ────────────────────────────────────
if ($errno !== 0) {
self::saveError($cache_path, "cURL error $errno: " . curl_strerror($errno));
return;
}
if ($http_code < 200 || $http_code >= 300) {
self::saveError($cache_path, "HTTP error $http_code");
return;
}
$res = json_decode($response, true);
if (!isset($res['candidates'][0]['content']['parts'][0]['text'])) {
self::saveError($cache_path, 'Gemini API レスポンスが不正です');
return;
}
// ─── 回答をキャッシュに保存 ────────────────────────────
$answer = trim($res['candidates'][0]['content']['parts'][0]['text']);
self::saveJson($cache_path, [
'message' => $answer,
'question' => $question,
'file' => $data['file'] ?? '',
'timestamp' => date('Y-m-d H:i:s'),
]);
}
// ──────────────────────────────────────────────────────────────
// 以下、ファイル読み書きのヘルパー
// ──────────────────────────────────────────────────────────────
/**
* キャッシュ .json を読み込んで配列で返す
* ファイルが存在しなければ空配列を返す
*
* @param string $cache_path .json ファイルの絶対パス
* @return array
*/
public static function read(string $cache_path): array
{
if (!file_exists($cache_path)) {
return [];
}
$raw = file_get_contents($cache_path);
if ($raw === false) {
return [];
}
$data = json_decode($raw, true);
return is_array($data) ? $data : [];
}
/** @internal */
private static function saveError(string $path, string $msg): void
{
self::saveJson($path, [
'error' => true,
'message' => $msg,
'timestamp' => date('Y-m-d H:i:s'),
]);
}
/** @internal */
private static function saveJson(string $path, array $data): void
{
// ディレクトリがなければ作成
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents(
$path,
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);
}
}
2. 使い方(呼び出し側)
require_once 'GeminiAnswer.php';
// キャッシュファイルのパス(質問ごとに一意なファイル名にする)
$cache_dir = DIR . '/cache/';
$cache_file = $cache_dir . 'bemani-question.json';
// ── キャッシュがなければ API を呼び出して保存 ──
if (!file_exists($cache_file)) {
GeminiAnswer::generate($cache_file, [
'message' => 'BEMANIシリーズについて教えて',
'file' => 'bemani-question',
]);
}
// ── キャッシュから読み込んで表示 ──
$data = GeminiAnswer::read($cache_file);
if (!empty($data['message'])) {
echo '
'; echo nl2br(htmlspecialchars($data['message'], ENT_QUOTES, 'UTF-8'));
echo '
';}
3. キャッシュ JSON の中身(例)
API 呼び出し後に保存される .json ファイルはこんな形になります。
{
"message": "BEMANIシリーズとは、コナミが展開する音楽ゲームのシリーズです。...",
"question": "BEMANIシリーズについて教えて",
"file": "bemani-question",
"timestamp": "2026-02-27 12:00:00"
}
ポイントまとめ
| 項目 | 詳細 |
|---|---|
| API | Gemini 3.0 Flash (gemini-3.0-flash) |
| キャッシュ | .json ファイルに保存、2回目以降は API 不要 |
| タイムアウト | 接続 5 秒 / 応答 30 秒 |
| トークン上限 | maxOutputTokens: 2000 |
| エラー処理 | cURL / HTTP エラーをキャッシュに記録して表示は行わない |
キャッシュファイルを削除すれば次のアクセスで再生成されます。
運用上は定期的に古いキャッシュを掃除するクーロンジョブを用意すると良いかもしれませんね。
*この記事は実際にこのブログで動いている実装をベースに書きました。*
旧式
n8n を使った使用例: