__PHP_Incomplete_Class を防ぐ富豪アプローチ

調べると session start が auto になってて必要なファイルが読み込めてませんとかいう間抜けな事例が多いこの問題は、別に session とは無関係にオブジェクトのシリアライズを使う場面でよく出くわす。

原因はいたってシンプルで、unserialize() しようとしているオブジェクトの必要としているファイルがすべて揃っていないとこういう現象が起きる。

そもそも unserialize() してできあがったオブジェクトというのはクラス名そのものは保持しているけれどもクラス定義も、もちろんメソッドも保持していないオブジェクトで、自身が保持しているクラス名の定義を、現在読み込んである PHP スクリプトから探し出して当てはめようとする。

当然この段階で include されていないクラス定義を利用することはできず、結果、__PHP_Incomplete_Class という特殊なクラスの宙ぶらりんなオブジェクトができあがる。くり返す。これは変数しか持っていない。メソッドと変数をパッケージにしたものがオブジェクトなのだから、メソッドがないオブジェクトなんて何の役にも立たない。

解決方法も簡単である。unserialize() の時点で必要なファイルがすべて読み込まれていればよい。

でもさ。読み込んだ先のクラスを定義しているファイルで読み込んでいるファイルも、ぜーんぶ準備してから unserialize() なきゃいけないってことになったらそのクラスは生のファイルの情報については何も隠蔽してくれないへっぽこクラスだと思わないかい?

そこでこういうのはどうだろう。

  1. すべての基本になるクラスを用意する
  2. そいつに以下のコードを用意する
function __sleep() {
  $this->_included = get_included_files();
  return array_keys( get_object_vars( $this ) );
}

function __wakeup() {
  if ( is_array( $this->_included ) ) {
    foreach ( $this->_included as $file ) {
      require_once( $file );
    }
  }
}

これでとりあえずクラスを定義しているファイルを読み込み忘れて incomplete なんちゃらになっちゃうミスは防げる。何段階にも読み込んでいて何が何やら訳が分からなくなっててもクラス定義を再現できる。

あーめんどくせ。

注意1 不必要なファイルの読み込みとクラスの再定義

問答無用で利用していたファイルを読み込み直すと、オブジェクトの呼び出し方によっては不必要なファイルを読み込むことがある。単に不要なだけなら問題ないが、このとき、クラス名がかぶっていると問題が起きる。PHP は同名のクラスを再定義することができないが、これは warning などではなく Fatal Error なのでそこでスクリプトがストップしてしまう。

PHP には名前空間がないし、これを避ける方法を考えるのは、とりあえず今は面倒くさい。ちゅーことで名前がかぶってるクラスを作るときには

if ( !class_exists( 'KLASS' ) ) {
  class KLASS {
  }
}

と定義する。work around バンザイ。どちくしょう。

注意2 ファイル名のリストによる serialized object の容量増大

オブジェクトの配列など、大量のオブジェクトを上の方法で serialize すると容量のふくれ方が半端でない。その場合はそれぞれのメソッドを上書きして

function __sleep() {
  return array_keys( get_object_vars( $this ) );
}

function __wakeup() {
  // 絶対に必要なファイルを自力 include
}

にしておくとよい。あるいはファイル名のリストを serialize して zlib で圧縮するという方法があるかもしれないが、serialize したオブジェクトの容量のふくれ方や必要なファイルの特定しやすさなどを加味して決めればよい。

More