作成日:

PHP だけで Gemini API を Q&A 形式で呼び出す実装メモ

n8n などの外部 Workflow ツールに頼らず、PHP + ファイルキャッシュ だけで

Gemini API を Q&A 形式で呼び出す仕組みを作ったので紹介します。

Gemini API 回答生成イメージ (Gemini 画像生成)
Gemini API 回答生成イメージ 🔎(元画像) (Gemini 画像生成)

概要

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"

}

ポイントまとめ

項目詳細
APIGemini 3.0 Flash (gemini-3.0-flash
キャッシュ.json ファイルに保存、2回目以降は API 不要
タイムアウト接続 5 秒 / 応答 30 秒
トークン上限maxOutputTokens: 2000
エラー処理cURL / HTTP エラーをキャッシュに記録して表示は行わない

キャッシュファイルを削除すれば次のアクセスで再生成されます。

運用上は定期的に古いキャッシュを掃除するクーロンジョブを用意すると良いかもしれませんね。

*この記事は実際にこのブログで動いている実装をベースに書きました。*

旧式

n8n を使った使用例:

webhooks を使った AI 投稿テスト

試験的に Google Gemini AI をつかった description 生成を始めてみました