symfony 1のオートロードのワナ

symfony 1ではproject, apps, modulesのそれぞれのディレクトリの中の"lib"というディレクトリにあるクラスはすべてオートロードの対象になります。apps, modulesに関しては現在実行中のアプリケーションとモジュールのlib以下のみが対象になります。ここまでは基本です。

オートロードの内部の実装を軽く説明しておきましょう。frontendはアプリケーション名だと思ってください。

最初はプロジェクトのlib以下にあるファイルから.php再帰的に探します。.phpが見つかった場合、ファイルを読み込んで(file_get_contents)、class/interface定義を検索し、クラス名をキー、クラスファイルの絶対パスを値にして配列に格納します。この操作をすべての.phpファイルに適応させて、大きな連想配列を作成します。なお、プロジェクトのlibの中からはすべてのディレクトリではなく、ディレクトリ名がmodel・symfony・vendorのいずれかの場合は無視します。

プロジェクトのlibを走査した後は、lib/model、apps/frontend/lib、apps/frontend/modules/*/libの順で検索し、連想配列に変換します。moduleの場合、アプリケーション内の全moduleの中から探しますが、その際にキーとなるクラス名に直接クラス名を入れず、「モジュール名/クラス名」という形式でいれます。

最終的に「クラス名 => クラスファイルのパス」形式の大きな連想配列ができあがり、それをcache/frontend/${env}/config/config_autoload.yml.phpというキャッシュファイルに出力します。そのキャッシュファイルを読み込んで、オートロードを行うクラスのプロパティとして保持します。オートロード実行時には、先ほどの配列の中からクラス名を元にファイルを探して読み込むだけ、というわけです。

symfony 1はこのようなオートロードの仕組みなので、ディレクトリ名やクラス名などは特に意識せず好きなだけディレクトリをわけてしまえばいいわけです。

問題

問題になるのはapps/frontend/modules/*/libの中です。キャッシュに落とすまではなんら問題ありません。問題は実際にオートロードを行う段階です。

読み込む際に、modules以下は「モジュール名/クラス名」という形式になると書きました。ということは、モジュール名が取得できないと読み込みはできません。当然ですね。

モジュール名を取得する、という処理はsfContext::getModuleName()で取得します。ではこのメソッドの内部では何を行っているかというと、sfActionStack::getLastEntry()を実行して、現在実行中のアクションに対応するsfActionStackEntryを取得します。そこから$lastEntry->getModuleName()を呼び出しています。このlastEntryはプロパティとしてモジュール名を持っているため、とりあえずlastEntryのインスタンスがあればいいのです。

しかし、ここからが問題なのです。Entryの追加はsfController::forward()で行っていますが、このsfActionStackEntryのコンストラクタを見ればわかるとおりsfActionのインスタンスが必要になります。つまり、アクションのインスタンスを作成して、それを元にEntryクラスを作成するまでは、モジュール名の特定ができないため、モジュールのlib以下にあるクラスはオートロードの対象にはならない、ということです。

これが問題になるのは、アクションをインスタンス化する際です。実際にどう問題になったかというと、アクションを個別のクラスファイルに切り出していて、それぞれの親クラスを共通化したBaseModActionクラスを作成し、モジュールのlib/BaseModAction.class.phpに配置したところ、アクションをインスタンス化する段階ではBaseModActionクラスがオートロードに引っかからずにエラーになってしまったというわけです。

対策

  • require_onceする
  • どうしてもオートロードしたいならapps/frontend/libに置く


もうsymfonyの情報を探るときはドキュメントとか一切読まずにソース追うので、もしかしたら有名だったりするのかもしれませんが・・・。前に似たようなことやろうとしてちゃんとrequire_onceしてるソースを見たことがあったような気もします。

これでも、キャッシュができちゃえばいいけど、キャッシュ作るのって相当コストかけてますよね。Symfony 2はSplClassLoaderがベースなのでそっちはオートロードにキャッシュとかは使わないでしょうけど。

あとそうそう、僕がSymfony 2の情報をあまり出していないのには理由があるんです。中はちょくちょく追ってるし動かしてもいるんですけど、挙動が理解できてない点がまだあるところと、まだ実装が大胆に変更されているという点です。この前ちょっとコミットしたんですけど、最近RequestHandler\RequestのGETやPOSTパラメータの値の持ち方が大きく変わって、RequestBagという1系のsfParameterHolderのようなクラスが使われるようになったのですが、パラメータを取得する側も呼び出し箇所を修正せざるを得ないような変更で、しかもかなり重要なクラスなのにテストも書いてなくて微妙にバグもあったという状況です。

Fabienさんがものすごい勢いで開発してるのは、最近のコンポーネントのリリース状況を追っている方ならわかるでしょう。コアな部分もその勢いのまま修正されたりしています。時間がなくて細かく検証できない状況なので、中途半端な情報を記述するよりは、ある程度落ち着くまではじっくりと内部を調べて、落ち着いてきたなと思ったら情報を書いていこうと思います。