職業プログラマの休日出勤

職業プログラマによる日曜自宅プログラミングや思考実験の成果たち。リアル休日出勤が発生すると更新が滞りがちになる。記事の内容は個人の意見であり、所属している(いた)組織の意見ではない。

CakePHPのTableRegistryでのテーブル探索

CakePHP 5.x で新しいアプリケーションを開発して、いざ本番環境に初めてdeployしたところ、以下のようなエラーに遭遇しました。

error: [Cake\ORM\Exception\MissingTableClassException] Table class for alias `{テーブル名}` could not be found

この記事は、このエラーの原因のうち、筆者が遭遇したものについてのメモです。

長いので3行まとめ

  • Macのデフォルトのような case-insensitive なボリュームで開発されたコードは、case-sensitive 環境では動作しないことがある(PHPの auto_load が)
  • CakePHPTableRegistry::getTableLocator()->get();が本来想定しているのは、テーブル名ではなく、Tableクラスのクラス名から「Table」を取り除いた文字列(documentから読み取れる情報)
  • そんな文字列はバグの原因になるから渡したくねーよ、という私のような人は FooBarTable::class みたいな形で渡すと幸せになれる🥰

問題のコード

ControllerやCommand(CLIコマンド)のコードで、以下のコードが「ローカル開発環境では動作するけれども、本番ではエラーになるコード」でした。

$t = TableRegistry::getTableLocator()->get('{テーブル名}');

ここでは、 {テーブル名} は、全て英小文字で構成された文字列です。

なぜ?

TableLocatorの get() 関数は、デフォルトでは vendor/cakephp/cakephp/src/ORM/Locator/TableLocator.php のものが呼ばれます。
このget関数は親クラスである AbstractLocator.php のものでキャッシュ管理されます。キャッシュヒットしなかった場合は、子クラスである TableLocatorcreateInstance 関数が呼ばれて、Tableのインスタンスを作るという仕組みになっています。
なので、エラーの原因を探る時に読むべきは、子クラスの createInstance ということになります。

以下は、CakePHP 5.0.5 における createInstanceソースコードへのリンクです。

github.com

アプリを組む側としては、ここで指定するのは「テーブル名」であって「Tableクラスの名前」ではないような感覚*1に陥りますが、このソースコードを読む限り、テーブル定義を見に行っている様子はぱっと見では見受けられず、「Tableクラス」を検索しに行っているように見えます。

途中に出てくる _getClassName関数 の呼び出しを追いかけていくと、最終的には class_exists 関数に辿り着きます。

www.php.net

これを見て、ふと疑問に思ったのは、「もしかして auto_load の環境では、この関数は case sensitive な動きをしたりしなかったりするのでは?」ということでした。
その旨、検索すると以下の StackOverflow のQ&Aに到達しました。ビンゴ!です。

stackoverflow.com

auto load するクラスを探索するとき、ファイルシステムcase-insensitive だったら、クラス名の検索も case-insensitive になるっぽいです。

筆者は普段はきちんと大文字始まりの文字列を、ドキュメントの慣習に従って渡していましたが、意識としてはテーブル名という意識でした。今回は偶然にも誤って小文字で渡していました。

再発防止策

CakePHPに限った話ではありませんが、クラス名とか関数名とかを文字列で渡す文化は、静的型付け言語を好む私には馴染みません(それでも使い続けますが😆)。
でも、CakePHPは親切なことに、このTableLocatorにおいては、名前空間や末尾に Table を伴っていても上手くクラスを検索してくれますので、Tableクラスへの参照を使って TableRegistry::getTableLocator()->get(FooBarTable::class);のような書き方をすれば、大文字小文字を誤ってfooBarTable::class としても、完全修飾クラス名としては正しい大文字小文字設定の文字列がruntimeにおいて得られるようなので、確実に幸せになれます。typoしたらIDEとかが怒ってくれるという点も、幸せポイント増し増しですね。

バージョン情報等

  • CakePHP 5.0.5
    • 補足:本件と同じことは恐らく CakePHP 3.x や 4.x でも起きるだろうと思います。
  • ローカル開発環境
  • 本番環境

*1:個人の感想です