PHPのセッションをMySQLで管理する際につまずいたところのTIPS
通常、サーバー内にファイルとして管理されるセッションデータを、データベース側で管理したくてMySQLでテーブルを作って管理することにした。
それでPHP5.4以降から使えるSessionHandlerInterfaceを継承して行うことにしたのだが、今までファイルやフレームワークでセッションを管理していた癖のせいでPHPのドキュメンテーションを見ながらコマネチすることになったのだが・・・。
Fatal error: session_start(): Failed to initialize storage module: user (path: ) とかいうエラーから始まり、四苦八苦してMySQLの対象テーブルに辿り着こうものなら、SessionHandlerInterfaceのwriteが動かない、セッションのデータを更新できない。
今回はこれらを解決・・・というか、これらのエラーに関してはほとんど凡ミスに集約されるので、同じことでつまずかないように書いておこうと思う。
2019/03/31 追記と補足:
session_regenerate_id()の対応のため、create_sid()の存在に気付きました。
これと併せてこちらの記事もどうぞ。
とりあえず、今回書いたコードをざっくり再現。
(あくまで再現なので、実行はしてない。)
Class My_Sessions { function __construct() { $handler = new My_Sessions_Handler(); session_set_save_handler( $handler, true ); session_start(); } } Class My_Sessions_Handler implements SessionHandlerInterface { private $db; function __construct() { $this->db = new mysqli( 'host', 'user', 'pass', 'name' ); } public function close() { $this->db->close(); return true; } public function destroy( $id ) { $sql = sprintf( 'DELETE FROM table_sessions WHERE sid = %1$s;', $this->db->real_escape_string( $sid ), $this->db->real_escape_string( $sdata ), $this->db->real_escape_string( $updated ) ); $this->db->query( $sql ); return true; } public function gc( $maxlifetime ) { // 今回はあまり関係ないので割愛 return true; } public function open( $savePath, $sessionName ) { return true; } public function read( $id ) { $sql = sprintf( 'SELECT * FROM table_sessions WHERE sid = %1$s;', $this->db->real_escape_string( $id ) ); $row = $this->db->query( $sql )->fetch_assoc(); if ( ! empty( $row['sdata'] ) ) { return $row['sdata']; } return ''; } public function write( $id, $data ) { // 191004にUPDATE追加して修正 $now = date( 'Y-m-d H:i:s' ); $sql = sprintf( 'SELECT sid FROM table_sessions WHERE sid = %1$s;', $this->db->real_escape_string( $id ) ); if ( 1 <= count( $this->db->query( $sql ) ) ) { $sql = sprintf( 'UPDATE table_sessions SET sid = %1$s, sdata = %2$s, updated = %3$s, WHERE sid = %4$s;', $this->db->real_escape_string( $id ), $this->db->real_escape_string( $data ), $this->db->real_escape_string( $now ), $this->db->real_escape_string( $id ) ); } else { $sql = sprintf( 'DELETE FROM table_sessions WHERE sid = %1$s;', $this->db->real_escape_string( $id ) ); $this->db->query( $sql ); $sql = sprintf( 'INSERT INTO table_sessions (sid, sdata, updated) VALUES (%1$s, %2$s, %3$s);', $this->db->real_escape_string( $id ), $this->db->real_escape_string( $data ), $this->db->real_escape_string( $now ) ); $this->db->query( $sql ); } return true; } } $session = new My_Sessions();
とりあえずはこんな感じ。
なんというかドキュメンテーション通りの教科書通りですね。
・・・まぁ実際に自分が書いていたコードは、PHPとMySQLとの相互のアクセスを自分がやりやすいように、上記のように直でSQL文を書いていたりなんかしていないけど・・・まぁ雰囲気だけ伝わればいいかな。
※これが今回のつまづきの要因のひとつでもあるんやけど。
それはともかく、まずは先にFatal error: session_start(): Failed to initialize storage module: user (path: ) とかいうエラーから片付ける。
これは、おおよそのところ「セッションのデータを格納する場所を指定できていない」から発生するエラーだと思っていいみたい。
サーバー環境を構築する際にセッションのパスが指定できていないから発生する。
なので、このエラーについては単にパスが指定できていないから!!これで終わり!!!111
・・・。
・・・いやいや、そんなことで単純なことで解決できてたらそもそも悩む必要とかいらねぇwww
実際のところで、自分がこのエラーを発生させたのは、きちんと完成されたサーバー環境だ。
ファイル単体のセッションならもちろんきちんと動いているので、これには該当しない。
で、それなのになぜ「セッションのデータを格納する場所を指定できていない」のか?
ここらへんもいろいろいじくって調べてみたが、要はファイルの代わりにデータベースを指定した際に、
- データベースの指定が間違っている(→ホストとかパスワードちゃんと合ってるぅ?)
- 対象のテーブルが作成できていない(→テーブルがなけりゃ保存先はそもそもない)
- SessionHandlerInterfaceに返す戻り値が変(→falseとかになってなぁい?)
- SessionHandlerInterfaceへの戻り値がプログラム的に間違ってなくても、そもそもSQL構文がうまく発行できていないので返す戻り値がfalse(そもそものSQL構文の見直しが必要)
とかのもろもろの要因で、セッションの保存先が既定値から空となってしまい、「セッションのデータを格納する場所を指定できていない状態」になってしまっているためである。
ファイルからMySQLにセッションの保存先を変更する場合、件のエラーが発生したらまずはこの辺りを一つ一つ見直せば、自ずと解決する。
で、第二に、SessionHandlerInterfaceのwriteが動かない件について。
これはwriteメソッド単体で発生しているのではなく、その他のメソッドも密接に関係している。
具体的に言うとreadメソッド。
SessionHandlerInterfaceに関して、PHPのドキュメンテーションのプログラム構成をコピペでそのまま持ってきているとまず目を通さないかもしれないが、session_set_save_handler()のページに答えは載っている。
ご丁寧にreadメソッド、writeメソッドの両方に書いてあった。
read(string $sessionId)
~~~
このコールバックが返す値は、 write コールバックがストレージに渡した形式とまったく同じシリアライズ形式でなければなりません。 返された値を PHP が自動的にアンシリアライズして、[…]
write(string $sessionId, string $data)
~~~
このコールバックに渡されたシリアライズ後のセッションデータを、 渡されたセッション ID に対応させて格納しなければなりません。 このデータを取得した read コールバックは、 write コールバックに最初に渡されたのとまったく同じ値を返さなければなりません。
いやいや、そりゃ読み込んでから書き込み(更新)するんだから、データの変更するまでは一緒のデータって当然でしょwwww
とか思って、自分の書いたコードを見返してみた。
はい!すんません!!同じデータ返してませんでした!!!
今回のつまづきの要因のひとつでもあるんやけど・・・と先に書いていたが、フラグ回収ご苦労様でした^^^^^
何が原因だったかというとmysqliでSELECTで戻してくる配列を、自分の扱いやすいようにしておいたがために、上記のコードで言うところのsdataの添字に対応するデータじゃなくて全く別のデータが入っていたから。
バカじゃねぇの、俺(´・ω・`)
まぁ上記のコードみたく生のSQLを投げておけばそもそも起きないであろうエラー。
(でも、生のSQLを関数なりメソッドなりのいたるところに書いてあるシステムってまず見かけないけど・・・というかそんな書き方したらなんというか、いろいろヤバイ)
まぁこれはいい例で、SessionHandlerInterfaceのwriteが動かない場合は、引数$dataにreadと同じデータが入っているか、もしくはreadで戻す値がwriteの$dataと合致しているかを見ればいいということ。
まぁ多種多様なCMSやフレームワークが出てきている中で、ずっとそれにかまけてネイティブなPHPを触らないとこんな変なところでつまづいたりする。
特にCMSとか普通に扱うだけなら、MySQLはただのデータファイルの保存先みたいな感覚だもんな。
つか、普段WP触っているときにSQL文を発行したことって数える程度しかねーわ。
デジタルな部分のものづくりをするヤツにあるまじきwwww
それを考えたらまぁ今回のはいいケースワークかな・・・?(負け惜しみ&涙目)
やっぱりサーバーサイドをつま先程度でも触る機会があるのなら、ネイティブなコードはきちんと理解できるようになるべきだと感じる今日このごろ。