はじめに
PHPのセッションはデフォルトでファイルシステムに保存されますが、本番環境ではデータベースやRedis・Memcachedへの保存が求められる場面が多くあります。複数サーバー構成でのセッション共有、セッションデータの監査ログ、有効期限の細かい制御など、標準のファイルベースハンドラでは対応できないケースに対応するのが session_set_save_handler() です。
この関数を使うと、セッションの**open・close・read・write・destroy・gc(ガベージコレクション)**という6つの操作を自分で実装したカスタムハンドラに差し替えられます。PHP 5.4以降は SessionHandlerInterface を実装したクラスを渡す形式が主流となり、よりオブジェクト指向的に扱えるようになっています。
関数の概要
| 項目 | 内容 |
|---|---|
| 関数名 | session_set_save_handler() |
| 所属 | PHP セッション関数 |
| 導入バージョン | PHP 4以降(クラス形式は PHP 5.4以降) |
| PHP 8.x | 対応済み |
構文
クラス形式(PHP 5.4以降・推奨)
session_set_save_handler(SessionHandlerInterface $handler, bool $register_shutdown = true): bool
コールバック形式(後方互換)
session_set_save_handler(
callable $open,
callable $close,
callable $read,
callable $write,
callable $destroy,
callable $gc,
callable $create_sid = ?,
callable $validate_sid = ?,
callable $update_timestamp = ?
): bool
戻り値
- 成功時:
true - 失敗時:
false
$register_shutdown パラメータ
| 値 | 動作 |
|---|---|
true(デフォルト) | 内部で session_register_shutdown() を自動呼び出し |
false | 自動登録しない。session_register_shutdown() を明示的に呼ぶ必要がある |
通常は true のままで問題ありませんが、シャットダウン処理の順序を細かく制御したい場合は false にして手動管理します。
SessionHandlerInterface のメソッド一覧
| メソッド | 呼び出しタイミング | 役割 |
|---|---|---|
open(string $path, string $name): bool | セッション開始時 | 接続の確立など初期化処理 |
close(): bool | セッション終了時 | 接続のクローズなど後処理 |
read(string $id): string|false | セッションデータ読み込み時 | IDに対応するデータを返す(存在しなければ空文字) |
write(string $id, string $data): bool | セッションデータ書き込み時 | IDに対応するデータを保存する |
destroy(string $id): bool | session_destroy() 呼び出し時 | IDに対応するデータを削除する |
gc(int $max_lifetime): int|false | GC(ガベージコレクション)時 | 期限切れセッションを削除し、削除件数を返す |
基本的な使い方
<?php
class MySessionHandler implements \SessionHandlerInterface
{
public function open(string $path, string $name): bool { return true; }
public function close(): bool { return true; }
public function read(string $id): string|false { return ''; }
public function write(string $id, string $data): bool { return true; }
public function destroy(string $id): bool { return true; }
public function gc(int $max_lifetime): int|false { return 0; }
}
$handler = new MySessionHandler();
session_set_save_handler($handler, true);
session_start();
$_SESSION['user'] = 'alice';
実践的なクラスベースの使用例
例1:MySQL(PDO)によるセッション保存ハンドラ
<?php
/**
* MySQLにセッションデータを保存するカスタムハンドラ
*
* テーブル定義:
* CREATE TABLE sessions (
* id VARCHAR(128) NOT NULL PRIMARY KEY,
* data MEDIUMBLOB NOT NULL,
* expires_at DATETIME NOT NULL,
* created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
* INDEX idx_expires (expires_at)
* ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
*/
class PdoSessionHandler implements \SessionHandlerInterface
{
private \PDO $pdo;
public function __construct(\PDO $pdo)
{
$this->pdo = $pdo;
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
}
public function open(string $path, string $name): bool
{
return true; // PDO接続はコンストラクタで確立済み
}
public function close(): bool
{
return true;
}
public function read(string $id): string|false
{
$stmt = $this->pdo->prepare(
'SELECT data FROM sessions WHERE id = :id AND expires_at > NOW()'
);
$stmt->execute([':id' => $id]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
return $row ? $row['data'] : '';
}
public function write(string $id, string $data): bool
{
$maxLifetime = (int) ini_get('session.gc_maxlifetime');
$stmt = $this->pdo->prepare(
'INSERT INTO sessions (id, data, expires_at)
VALUES (:id, :data, DATE_ADD(NOW(), INTERVAL :ttl SECOND))
ON DUPLICATE KEY UPDATE
data = VALUES(data),
expires_at = VALUES(expires_at)'
);
return $stmt->execute([
':id' => $id,
':data' => $data,
':ttl' => $maxLifetime,
]);
}
public function destroy(string $id): bool
{
$stmt = $this->pdo->prepare('DELETE FROM sessions WHERE id = :id');
return $stmt->execute([':id' => $id]);
}
public function gc(int $max_lifetime): int|false
{
$stmt = $this->pdo->prepare('DELETE FROM sessions WHERE expires_at < NOW()');
$stmt->execute();
return $stmt->rowCount();
}
}
// --- 登録と使用 ---
/*
$pdo = new PDO('mysql:host=localhost;dbname=myapp;charset=utf8mb4', 'user', 'pass');
$handler = new PdoSessionHandler($pdo);
session_set_save_handler($handler, true);
session_name('AppSession');
session_start();
$_SESSION['user_id'] = 42;
*/
例2:Redis によるセッション保存ハンドラ
<?php
/**
* Redis にセッションデータを保存するカスタムハンドラ
* pecl/redis 拡張を使用
* 複数サーバー構成でのセッション共有に適している
*/
class RedisSessionHandler implements \SessionHandlerInterface
{
private \Redis $redis;
private int $ttl;
private string $prefix;
public function __construct(
string $host = '127.0.0.1',
int $port = 6379,
int $ttl = 1800,
string $prefix = 'sess_',
string $auth = ''
) {
$this->ttl = $ttl;
$this->prefix = $prefix;
$this->redis = new \Redis();
$this->redis->connect($host, $port);
if ($auth !== '') {
$this->redis->auth($auth);
}
}
public function open(string $path, string $name): bool
{
return $this->redis->isConnected();
}
public function close(): bool
{
return true; // 接続はコンストラクタで管理
}
public function read(string $id): string|false
{
$data = $this->redis->get($this->prefix . $id);
return $data !== false ? $data : '';
}
public function write(string $id, string $data): bool
{
return $this->redis->setex($this->prefix . $id, $this->ttl, $data);
}
public function destroy(string $id): bool
{
$this->redis->del($this->prefix . $id);
return true;
}
public function gc(int $max_lifetime): int|false
{
// Redis はキーの TTL を自動管理するため GC 不要
return 0;
}
/**
* Redis に保存されている全セッション数を返す(デバッグ用)
*/
public function countActiveSessions(): int
{
$keys = $this->redis->keys($this->prefix . '*');
return count($keys);
}
}
/*
$handler = new RedisSessionHandler(
host: 'redis.internal',
port: 6379,
ttl: 3600,
prefix: 'myapp_sess_',
auth: 'secret'
);
session_set_save_handler($handler, true);
session_start();
*/
例3:暗号化セッションハンドラ(既存ハンドラのデコレータ)
<?php
/**
* 既存のセッションハンドラをラップして、
* セッションデータを AES-256-GCM で暗号化するデコレータクラス
* データ漏洩時にセッション内容を保護する
*/
class EncryptedSessionHandler implements \SessionHandlerInterface
{
private string $key; // 32バイトの暗号化キー
public function __construct(
private readonly \SessionHandlerInterface $inner,
string $encryptionKey
) {
if (strlen($encryptionKey) !== 32) {
throw new \InvalidArgumentException('暗号化キーは32バイト(256bit)である必要があります');
}
$this->key = $encryptionKey;
}
public function open(string $path, string $name): bool
{
return $this->inner->open($path, $name);
}
public function close(): bool
{
return $this->inner->close();
}
public function read(string $id): string|false
{
$encrypted = $this->inner->read($id);
if ($encrypted === '' || $encrypted === false) {
return '';
}
return $this->decrypt($encrypted) ?? '';
}
public function write(string $id, string $data): bool
{
return $this->inner->write($id, $this->encrypt($data));
}
public function destroy(string $id): bool
{
return $this->inner->destroy($id);
}
public function gc(int $max_lifetime): int|false
{
return $this->inner->gc($max_lifetime);
}
private function encrypt(string $plaintext): string
{
$iv = random_bytes(12); // GCM推奨IVサイズ
$tag = '';
$ciphertext = openssl_encrypt(
$plaintext, 'aes-256-gcm', $this->key,
OPENSSL_RAW_DATA, $iv, $tag
);
// IV + Tag + 暗号文を Base64 エンコードして保存
return base64_encode($iv . $tag . $ciphertext);
}
private function decrypt(string $encoded): ?string
{
$decoded = base64_decode($encoded, true);
if ($decoded === false || strlen($decoded) < 28) {
return null;
}
$iv = substr($decoded, 0, 12);
$tag = substr($decoded, 12, 16);
$ciphertext = substr($decoded, 28);
$result = openssl_decrypt(
$ciphertext, 'aes-256-gcm', $this->key,
OPENSSL_RAW_DATA, $iv, $tag
);
return $result !== false ? $result : null;
}
}
/*
$pdo = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'pass');
$inner = new PdoSessionHandler($pdo);
$handler = new EncryptedSessionHandler($inner, str_repeat('k', 32)); // 本番はenv変数から取得
session_set_save_handler($handler, true);
session_start();
*/
例4:セッション操作を監査ログに記録するハンドラ
<?php
/**
* セッションの read / write / destroy を監査ログに記録するデコレータ
* セキュリティ監査・不正アクセス調査・デバッグに使用する
*/
class AuditingSessionHandler implements \SessionHandlerInterface
{
private array $auditLog = [];
public function __construct(
private readonly \SessionHandlerInterface $inner
) {}
public function open(string $path, string $name): bool
{
$this->log('open', null, ['path' => $path, 'name' => $name]);
return $this->inner->open($path, $name);
}
public function close(): bool
{
$this->log('close');
return $this->inner->close();
}
public function read(string $id): string|false
{
$data = $this->inner->read($id);
$this->log('read', $id, ['found' => ($data !== '' && $data !== false)]);
return $data;
}
public function write(string $id, string $data): bool
{
$result = $this->inner->write($id, $data);
$this->log('write', $id, [
'size' => strlen($data),
'success' => $result,
]);
return $result;
}
public function destroy(string $id): bool
{
$result = $this->inner->destroy($id);
$this->log('destroy', $id, [
'success' => $result,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
]);
return $result;
}
public function gc(int $max_lifetime): int|false
{
$deleted = $this->inner->gc($max_lifetime);
$this->log('gc', null, ['deleted' => $deleted]);
return $deleted;
}
private function log(string $operation, ?string $sessionId = null, array $meta = []): void
{
$entry = [
'timestamp' => date('Y-m-d H:i:s'),
'operation' => $operation,
'session_id'=> $sessionId ? substr($sessionId, 0, 8) . '...' : null,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'meta' => $meta,
];
$this->auditLog[] = $entry;
// 実際にはファイルやDBへ書き込む
// file_put_contents('/var/log/app/session_audit.log',
// json_encode($entry, JSON_UNESCAPED_UNICODE) . "\n", FILE_APPEND);
}
public function getAuditLog(): array
{
return $this->auditLog;
}
public function printAuditLog(): void
{
echo "=== セッション監査ログ ===\n";
foreach ($this->auditLog as $entry) {
$meta = json_encode($entry['meta'], JSON_UNESCAPED_UNICODE);
printf(
"[%s] %-10s | ID: %-12s | %s\n",
$entry['timestamp'],
$entry['operation'],
$entry['session_id'] ?? '------',
$meta
);
}
}
}
/*
$pdo = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'pass');
$inner = new PdoSessionHandler($pdo);
$handler = new AuditingSessionHandler($inner);
session_set_save_handler($handler, true);
session_start();
$_SESSION['user_id'] = 1;
session_write_close();
$handler->printAuditLog();
*/
例5:SessionHandlerInterface + SessionUpdateTimestampHandlerInterface を実装した完全版ハンドラ
<?php
/**
* PHP 7.0以降で使える SessionUpdateTimestampHandlerInterface も実装した
* 完全版セッションハンドラ
*
* validateId() : セッションIDの有効性を検証する
* updateTimestamp() : データ変更なしでもタイムスタンプを更新する(セッション延長)
*/
class FullFeaturedSessionHandler implements
\SessionHandlerInterface,
\SessionUpdateTimestampHandlerInterface
{
private array $store = []; // メモリストア(デモ用)
public function open(string $path, string $name): bool
{
return true;
}
public function close(): bool
{
return true;
}
public function read(string $id): string|false
{
if (!isset($this->store[$id])) {
return '';
}
$record = $this->store[$id];
if ($record['expires_at'] < time()) {
unset($this->store[$id]);
return '';
}
return $record['data'];
}
public function write(string $id, string $data): bool
{
$ttl = (int) ini_get('session.gc_maxlifetime');
$this->store[$id] = [
'data' => $data,
'expires_at' => time() + $ttl,
'updated_at' => time(),
];
return true;
}
public function destroy(string $id): bool
{
unset($this->store[$id]);
return true;
}
public function gc(int $max_lifetime): int|false
{
$now = time();
$deleted = 0;
foreach ($this->store as $id => $record) {
if ($record['expires_at'] < $now) {
unset($this->store[$id]);
$deleted++;
}
}
return $deleted;
}
// --- SessionUpdateTimestampHandlerInterface ---
/**
* セッションIDがストレージに存在し、かつ有効かを検証する
* use_strict_mode が有効なときに呼ばれる
*/
public function validateId(string $id): bool
{
$valid = isset($this->store[$id]) && $this->store[$id]['expires_at'] >= time();
echo "validateId({$id}): " . ($valid ? 'valid' : 'invalid') . "\n";
return $valid;
}
/**
* セッションデータが変化しなかった場合でもタイムスタンプを更新する
* lazy_write が有効なときに呼ばれる
*/
public function updateTimestamp(string $id, string $data): bool
{
if (isset($this->store[$id])) {
$ttl = (int) ini_get('session.gc_maxlifetime');
$this->store[$id]['expires_at'] = time() + $ttl;
$this->store[$id]['updated_at'] = time();
echo "updateTimestamp({$id}): 延長しました\n";
return true;
}
return false;
}
public function dumpStore(): void
{
echo "=== ストア内容 ===\n";
foreach ($this->store as $id => $record) {
printf(
"ID: %-10s | size: %4d bytes | expires: %s\n",
substr($id, 0, 8) . '...',
strlen($record['data']),
date('H:i:s', $record['expires_at'])
);
}
}
}
$handler = new FullFeaturedSessionHandler();
session_set_save_handler($handler, true);
ini_set('session.use_strict_mode', '1');
session_start();
$_SESSION['demo'] = 'full_featured';
$handler->dumpStore();
例6:ファクトリパターンによるハンドラ選択クラス
<?php
/**
* 環境設定に応じて最適なセッションハンドラを生成・登録するファクトリクラス
* アプリケーションのブートストラップ処理で使用することを想定
*/
class SessionHandlerFactory
{
public function __construct(private readonly array $config) {}
/**
* 設定に基づいてハンドラを生成し session_set_save_handler() で登録する
*/
public function createAndRegister(): \SessionHandlerInterface
{
$driver = $this->config['driver'] ?? 'files';
$handler = match ($driver) {
'pdo' => $this->createPdoHandler(),
'redis' => $this->createRedisHandler(),
'memory' => new FullFeaturedSessionHandler(),
default => $this->createDefaultHandler(),
};
// 必要に応じてデコレータをスタック
if ($this->config['encrypt'] ?? false) {
$key = $this->config['encryption_key']
?? throw new \RuntimeException('暗号化キーが設定されていません');
$handler = new EncryptedSessionHandler($handler, $key);
echo "暗号化レイヤー: 有効\n";
}
if ($this->config['audit'] ?? false) {
$handler = new AuditingSessionHandler($handler);
echo "監査ログレイヤー: 有効\n";
}
session_set_save_handler($handler, true);
echo "ドライバ: {$driver}\n";
echo "ハンドラ登録完了: " . get_class($handler) . "\n";
return $handler;
}
private function createPdoHandler(): PdoSessionHandler
{
$dsn = $this->config['dsn']
?? throw new \RuntimeException('PDO DSN が設定されていません');
$pdo = new \PDO($dsn, $this->config['db_user'] ?? '', $this->config['db_pass'] ?? '');
return new PdoSessionHandler($pdo);
}
private function createRedisHandler(): RedisSessionHandler
{
return new RedisSessionHandler(
host: $this->config['redis_host'] ?? '127.0.0.1',
port: $this->config['redis_port'] ?? 6379,
ttl: $this->config['redis_ttl'] ?? 1800,
prefix: $this->config['redis_prefix'] ?? 'sess_',
auth: $this->config['redis_auth'] ?? ''
);
}
private function createDefaultHandler(): \SessionHandler
{
// PHP組み込みのデフォルトハンドラをそのまま使用
return new \SessionHandler();
}
}
// 利用例
$factory = new SessionHandlerFactory([
'driver' => 'memory', // pdo / redis / memory / files
'encrypt' => false,
'audit' => true,
]);
$handler = $factory->createAndRegister();
session_start();
$_SESSION['factory_demo'] = true;
/*
出力例:
監査ログレイヤー: 有効
ドライバ: memory
ハンドラ登録完了: AuditingSessionHandler
*/
デコレータパターンのスタック構成
session_set_save_handler() の真価はデコレータパターンとの組み合わせにあります。
リクエスト
↓
AuditingSessionHandler ← 監査ログ(外側)
↓
EncryptedSessionHandler ← 暗号化(中間)
↓
PdoSessionHandler ← 実際の保存先(内側)
↓
MySQL / Redis / etc.
$inner = new PdoSessionHandler($pdo);
$middle = new EncryptedSessionHandler($inner, $key);
$outer = new AuditingSessionHandler($middle);
session_set_save_handler($outer, true);
session_start();
関連関数・インターフェースとの比較
| 名前 | 種別 | 役割 |
|---|---|---|
session_set_save_handler() | 関数 | カスタムハンドラを登録する |
SessionHandlerInterface | インターフェース | 実装必須の6メソッドを定義 |
SessionUpdateTimestampHandlerInterface | インターフェース | validateId() / updateTimestamp() を追加(PHP 7.0以降) |
SessionHandler | クラス | PHP組み込みデフォルトハンドラ(継承して一部だけオーバーライドも可) |
session_module_name() | 関数 | 'user' を指定することで session_set_save_handler() と連動 |
session_register_shutdown() | 関数 | シャットダウン時の自動書き込みを登録($register_shutdown=false 時に必要) |
よくある落とし穴
<?php
// ❌ NG:session_start() の後に登録しても無効
session_start();
session_set_save_handler($handler, true); // 無視される
// ✅ OK:session_start() の前に登録する
session_set_save_handler($handler, true);
session_start();
<?php
// ❌ よくあるミス:read() が false を返すとセッションが壊れる
public function read(string $id): string|false
{
// 見つからなかった場合に false を返してしまう
return false; // ← NG
}
// ✅ 見つからない場合は空文字を返す
public function read(string $id): string|false
{
$data = $this->store[$id] ?? null;
return $data !== null ? $data : ''; // ← 空文字を返す
}
<?php
// ❌ 注意:write() の $data はシリアライズ済みのバイナリ文字列
// そのままDBの TEXT 型に入れると文字化けすることがある
public function write(string $id, string $data): bool
{
// TEXT型に入れると壊れる可能性がある
$stmt->execute([':data' => $data]);
}
// ✅ BLOB型(MEDIUMBLOB など)を使うか Base64 エンコードする
public function write(string $id, string $data): bool
{
$stmt->execute([':data' => base64_encode($data)]); // ← エンコードして保存
}
// read() では base64_decode() で戻す
まとめ
| 項目 | 内容 |
|---|---|
| 関数名 | session_set_save_handler(handler|callbacks, bool $register_shutdown = true): bool |
| 主な用途 | セッションの保存先をDBやRedisなどカスタムストレージに差し替える |
| 呼び出しタイミング | 必ず session_start() より前 |
| 推奨形式 | SessionHandlerInterface を実装したクラスを渡す形式(PHP 5.4以降) |
| デコレータ活用 | 暗号化・監査ログ・キャッシュなどをスタックして組み合わせられる |
read() の注意 | 見つからない場合は false ではなく空文字を返す |
| DB保存の注意 | $data はバイナリなので BLOB 型または Base64 エンコードを使う |
session_set_save_handler() は「セッションの保存先を完全に自分でコントロールしたい」すべての場面で活躍します。デコレータパターンを組み合わせることで、MySQL保存・暗号化・監査ログといった機能を関心事ごとに分離しながら積み重ねられる、非常に拡張性の高い設計が可能です。
