とあるお客様ECサイトで、「購入後に再購入しようとすると、変なエラーが出て買い物出来なくなる」で困っている。
担当しているシステム会社が対応してくれないから代わりに解決してほしい、と言われた事がありまして。
原因となるコード(PHP)をみつけたときに、「ああ、この書き方ね・・」と思うところがありましたので、そのシェアです。
問題となった内容
まず、問題となったコードのサンプルです。
問題のエッセンスだけ示しています。
foreach ( $_SESSION as $key => $value ) {
if ( 条件 ) unset ( $_SESSION[$key] );
}
処理後の値は一見問題無いのですが、このセッション変数をストレージ等に保存する時に保存されなかった。
要は次にアクセスした時にセッション変数が直前の状態になっていなかった。
という事象が発生したのです。
どう直したか
何が問題か、はさておき。
次の様に直したらその問題は解決しました。
$tmp_session_keyname = array();
foreach ( $_SESSION as $key => $val ) {
if ( 条件 ) $tmp_session_keyname[$key] = $key;
}
foreach ( $tmp_session_keyname as $key ) {
unset( $_SESSION[$key] );
}
何が問題なのか
foreach 文は PHP言語独自の機能で、配列を順番に繰り返し処理してくれるという便利なものです。
配列ですから、配列のキーを見ながら次はこのキー、というように処理するのでしょう。
最初の問題となったコードでは、今正に処理している配列の要素を unset、つまり消している事を行っています。
配列のデータ構造を、メモリ上にどう表現してどう処理するかイメージ出来る人であれば。
この書き方が「問題となる可能性がある」、と思う事が出来るのではないか、と思っています。
ちなみに、私が冒頭のコードを見て「ああ、この書き方ね・・」と思ったセンスはですね。
「今ループで回している値を消さないで!次に見る先は保証されるの?」
という不安感です。
問題となる例
- 配列のキーをメモリ上に順番に格納している
- 配列のキー領域には
(1)「配列の次のキーが格納されているメモリのアドレス」①が記録されている
(2) 配列の値が格納されているメモリのアドレス②が記録されている
と配列のデータ構造を定義している場合 - unset時は、データ以外にキーもメモリから解放するが、その前に以下を行う。
- データをメモリから解放した時に、データのガベージコレクションを行う
(データ領域の再配置を行うからメモリ位置も変わるので②も更新する) - unsetするキーの①を、unsetする1つ前に存在するキーの①にセットする。
- foreachループ時は、$_SESSION配列のキー領域をforeach用にコピーしたうえで、コピーに対しループ処理(アドレスの移動)を行っている。
こういう想像をした場合です。
あくまで例ですよ!本当にそうなっているわけではありません!
unsetをすると、$_SESSIONのキー情報は更新され、アドレスも変わります。
一方、foreachループ用キー領域は更新されないので、foreachループで次の配列の値を得る時に、今の$_SESSION変数とアドレスが変わっているため不整合が起きる。
もちろん、これは悪い作りをした場合の例であり、実際のPHP言語はそのあたりは問題ないでしょう。
実際に起きた事
実際には、$_SESSION変数の値を var_dump()しても期待した値に見えましたから、それ自体は問題がなかったです。
何が起きたかと言うと、その$_SESSION変数を保存する時に動く処理、session_set_save_handler()で定義された write()がコールされなかった、という事です。
セッション変数のシリアライズ化が上手くいかなかったのか、セッション変数が更新された、と認識してくれなかったのか、どちらかでしょう。
くどいですが。
PHPのソースコードを見て調べたわけではないので、あくまで推測です。
そして、全ての環境で再現する内容でもありませんでした。
特定のPHPバージョン、それに付随する環境でのみ起きた事でしょう。
私が言いたい事
私が言いたいことは、どういう結果になるか不明瞭なコード(問題が起きる可能性のあるコード)は書かないようにしましょう、という事です。
では、どういう場合に不明瞭なコード(問題が起きる可能性のあるコード)になるのか。
それは、コンピューターの基本を知って、上で「問題となる例」で書いた様な動きを想像できる様になれば分かるようになるでしょう、という事です。
この場合はデータ構造についての基礎知識です。
これを知っていれば、「問題となる例」で書いたことが起きる可能性があるから、冒頭で挙げた様なPHPスクリプトは最初から書かない、という発想が出来る様になる、という事です。
さらに私が言いたい事
PHPなどの処理系がどういう動きを想定しているのか、細かく知って下さい、という事ではありません。
例えば、以下の記事のように色々と調べてくれている人もいたりします。
https://ja.stackoverflow.com/questions/5770/php-foreach-の内部挙動について
もちろん、こういう形で労力とそこから得た情報を提供してくれる人は素晴らしいです。
しかし、このように言語の内部処理に依存するような書き方をするのではなく、依存しない書き方、曖昧じゃない書き方にすれば最初から悩まなくて済むわけです。
そして、冒頭で書かれているコード foreach中に unset()する事について、参照渡しじゃなくすれば大丈夫、みたいなことを書かれれている記事も多数みます。
しかし、私に言わせれば、それでも曖昧な結果になりそうなコードであることには変わりなく・・そんなコードは書かないでほしいのです。
なのですが。
伝わるかなー(汗