PHP のセッションストレージを MySQL に切替える方法
Web アプリケーションでは、ユーザーからの一連の HTTP 要求をまとめて考えることができるよう、しばしば「セッション」という考え方が導入されます。
例えば一旦ログインを行なったら、そのセッションをログイン済みとマークしておき、そのログイン済みセッション内ではログイン処理を繰り返さない、といったことが行なわれます。
各種フレームワークでセッションオブジェクト、セッション変数という形で出現するので、ウェブアプリケーションを開発したことのある方なら、一度は利用したことがあると思います。
蛇足ですが、セッション変数と聞くと反射的に、アパートメントスレッドモデルの COM オブジェクトのポインターをセッションオブジェクトに格納していて、 他のワーカースレッドからそれを利用し、メソッド呼び出しの際に元のスレッドにマーシャリングされてそこがボトルネックになるとか、デッドロックになってしまう・・・というトラブルが多発した2000年代前半のことを思い出しちゃいますね(苦笑)
ま、それはさておき、話を戻して、このページでは PHP のセッションデータを自前のストレージに格納する方法を説明します。
ここでは特に MySQL データベース内に保存する方法を示します。
PHP のセッション $_SESSION
PHP ではセッションを通して利用できる $_SESSION という変数に、キーと値のペアという形で保存します。
$_SESSION['a'] = 'XYZ' として保存すれば、同じセッション内で $_SESSION['a'] から 'XYZ' を取得することができるので非常に扱いが簡単です。
$_SESSION に 'a' というキーがセットされているかどうかは、isset($_SESSION['a']) で確認できます。
セッションの情報は php.ini で保存場所が指定できます。 session.save_handler に "files" を指定して、session.save_path にパスを指定すればそのフォルダにセッション情報を格納するファイルが出来上がるという仕組みです。
セッションハンドラの作成
PHP ではさらに、セッション情報の格納場所及び格納方法をきめ細かく変更できるようになっています。 session_set_save_handler 関数を使って、セッション情報の取得・格納を行なう関数をセットするだけで実装できます。
PHP 5.4 からは SessionHandlerInterface というインターフェイスを実装したクラスの参照を session_set_save_handler に渡すだけです。
SessionHandlerInterface は次の形をしています。
SessionHandlerInterface { /* Methods */ abstract public bool close ( void ) abstract public bool destroy ( string $session_id ) abstract public bool gc ( string $maxlifetime ) abstract public bool open ( string $save_path , string $name ) abstract public string read ( string $session_id ) abstract public bool write ( string $session_id , string $session_data ) }
SessionHandlerInterface では6個のメソッドがあります。
どのように呼ばれるかというと、$_SESSION を利用するために start_session() を呼びますが、ここで open と read が呼ばれ、 ストレージから $_SESSION へ値が読み込まれます。
そのページから抜けるときに、write と close が呼ばれます。
このタイミングでストレージに $_SESSION にセットした値をシリアライズできます。
gc は session_start() 内で PHP システムから呼び出されます。このときに PHP 設定の session.gc_maxlifetime にセットした値が渡されます。 ここで期限切れになった古いセッションデータのクリーンアップを行ないます。
gc は毎回呼び出されるわけではなく、PHP 設定の session.gc_divisor と session.gc_probability の値でおよその値がきまります。 session.gc_divisor に 1000、session.gc_probability に 1 が設定されていれば session_start() を呼び出した 1000回に 1 度(位の)割合で gc が呼び出されることになります。
destroy はセッションを破棄する場合 (session_regenerate_id() に true を渡して呼び出した場合や session_destroy() を明示的に呼び出した場合) 及び session_decode() が失敗したときに呼び出されます。
destroy にはセッション ID が渡されますので、それをキーとしてストレージ内のセッションデータを破棄します。
MySQL をストレージにしたときはどうなるの?
一般論は上記の通りですが、じゃぁ具体的に MySQL をユーザーレベルストレージとして選択した場合、それぞれのメソッドをどのように実装すればよいか説明します。
呼ばれる順番に言うと・・・
open - 何もしない
read - セッションID をキーにしてデータを読み込んで string で返す。
write - セッションID とデータが渡されるので素直に保存する。
close - 何もしない (open でデータベースをあけて、ここでクローズするほうが良い場合があれば、そうすれば良いでしょうが・・・)
gc - 期限切れのセッションデータを削除
destroy - セッションID をキーにして値をストレージから削除
となります。
MySQL-PHP セッションハンドラの実装例
実装例は次の通りです。
まず MySQL のデータベース内に次のようなテーブルを用意します。
CREATE TABLE `php_sessions` ( `session_id` varchar(100) NOT NULL DEFAULT '', `session_updated` int(10) unsigned NOT NULL DEFAULT '0', `session_data` text, PRIMARY KEY (`session_id`) );
ここで session_id にはセッションID というキーを保持し、session_updated には最終更新日のタイムスタンプを保持しており、 session_data には $_SESSION の(シリアライズされた)値が入ることを想定しています。
$_SESSION に格納された値を一列の文字列にシリアライズするのは PHP のフレームワークが自動的にしてくれます。またシリアライズした値から $_SESSION にうまいこと格納してくれるのも PHP がやってくれるので気にしなくて大丈夫です。
単に渡されたキーと値のペアを素直にデータベースに格納していくだけです。
ハンドラクラスは次の通りです。
<?php
class MySessionHandler implements SessionHandlerInterface {
function open($save_path, $name) {
return true;
}
function read($session_id) {
$session_data = '';
$db = get_db();
if($stmt = $db->prepare("
SELECT session_data
FROM php_sessions
WHERE session_id=?")){
$stmt->bind_param("s", $session_id);
$stmt->bind_result($session_data);
$stmt->execute();
$stmt->fetch();
$stmt->close();
$stmt=null;
}
$db->close();
$db=null;
return $session_data;
}
function write($session_id, $session_data) {
$affected_rows = 0;
date_default_timezone_set("UTC");
$session_updated = time();
$db = get_db();
if($stmt = $db->prepare("
INSERT INTO php_sessions (
session_id, session_updated, session_data)
VALUES(?, ?, ?)
ON DUPLICATE KEY UPDATE
session_updated=?,
session_data=?")){
$stmt->bind_param(
"sisis",
$session_id,
$session_updated,
$session_data,
$session_updated,
$session_data);
$stmt->execute();
$affected_rows = $stmt->affected_rows;
$stmt->close();
$stmt=null;
}
$db->close();
$db=null;
return $affected_rows ? true : false;
}
function destroy($session_id) {
$db = get_db();
if($stmt = $db->prepare("
DELETE FROM php_sessions
WHERE session_id = ?")){
$stmt->bind_param("s", $session_id);
$stmt->execute();
$stmt->close();
$stmt=null;
}
$db->close();
$db=null;
return true;
}
function close() {
return true;
}
function gc($maxlifetime) {
$db = get_db();
if($stmt = $db->prepare("
DELETE
FROM php_sessions
WHERE session_updated < ?")){
date_default_timezone_set("UTC");
$t = time() - $maxlifetime;
$stmt->bind_param("i", $t);
$stmt->execute();
$stmt->close();
$stmt=null;
}
$db->close();
$db=null;
return true;
}
}
?>
ここでタイムスタンプは UTC で保持しています。昨今のクラウド環境ではサーバーがどこにあるかというのは、あまり気にしたくないことですから、ローカル時間で保存するのは避けたほうが無難です。
get_db() は次のような DB ハンドルを返す関数としています。
<?php
function get_db() {
$db = null;
$db = new mysqli(
'127.0.0.1:3306',
'user id',
'your password',
'db name'
);
if($db->connect_errno){
die('Unable to connect');
}
return $db;
}
?>
このようなセッションハンドラクラスを用意して、session_start の前に次のように session_set_save_handler に渡します。
<?php
$mysql_sesshandler = new MySessionHandler();
session_set_save_handler($mysql_sesshandler, true);
session_start();
?>
上記で、$_SESSION に値を格納すると、MySQL に次のように値が保存されていくことが確認できるはずです。